Merge commit 'cbc74c986b0418bb6b5a9cb4741a7569937fe3e4' into patch_branch

Change-Id: I0b828830d458958b486cdbf003c9abb41357dd96
diff --git a/.gitignore b/.gitignore
index d80e0ff..fa4b4bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -170,6 +170,8 @@
 /test/lsudev
 /test/mmtty
 /test/mmrules
+/test/mmsmspdu
+/test/mmsmsmonitor
 
 /ModemManager-*-coverage.info
 /ModemManager-*-coverage/
diff --git a/README b/README
index 27d6101..ade8b77 100644
--- a/README
+++ b/README
@@ -15,7 +15,7 @@
 ModemManager is a DBus system bus activated service (meaning it's started
 automatically when a request arrives). It is written in C, using glib and gio.
 Several GInterfaces specify different features that the modems support,
-including the generic MMIfaceModem3gpp and MMIfaceModemCdma which provice basic
+including the generic MMIfaceModem3gpp and MMIfaceModemCdma which provide basic
 operations for 3GPP (GSM, UMTS, LTE) or CDMA (CDMA1x, EV-DO) modems. If a given
 feature is not available in the modem, the specific interface will not be
 exported in DBus.
@@ -33,4 +33,4 @@
 
 License.
 The ModemManager and mmcli binaries are both GPLv2+.
-The libmm-glib library is LGPLv2+.
\ No newline at end of file
+The libmm-glib library is LGPLv2+.
diff --git a/cli/mmcli-call.c b/cli/mmcli-call.c
index a871dbc..99c8b76 100644
--- a/cli/mmcli-call.c
+++ b/cli/mmcli-call.c
@@ -46,11 +46,14 @@
 static Context *ctx;
 
 /* Options */
-static gboolean info_flag; /* set when no action found */
-static gboolean start_flag;
-static gboolean accept_flag;
-static gboolean hangup_flag;
-static gchar *dtmf_request;
+static gboolean  info_flag; /* set when no action found */
+static gboolean  start_flag;
+static gboolean  accept_flag;
+static gchar    *deflect_str;
+static gboolean  join_multiparty_flag;
+static gboolean  leave_multiparty_flag;
+static gboolean  hangup_flag;
+static gchar    *dtmf_request;
 
 static GOptionEntry entries[] = {
     { "start", 0, 0, G_OPTION_ARG_NONE, &start_flag,
@@ -61,6 +64,18 @@
       "Accept the incoming call",
       NULL,
     },
+    { "deflect", 0, 0, G_OPTION_ARG_STRING, &deflect_str,
+      "Deflect the incoming call",
+      "[NUMBER]",
+    },
+    { "join-multiparty", 0, 0, G_OPTION_ARG_NONE, &join_multiparty_flag,
+      "Join multiparty call",
+      NULL,
+    },
+    { "leave-multiparty", 0, 0, G_OPTION_ARG_NONE, &leave_multiparty_flag,
+      "Leave multiparty call",
+      NULL,
+    },
     { "hangup", 0, 0, G_OPTION_ARG_NONE, &hangup_flag,
       "Hang up the call",
       NULL,
@@ -99,6 +114,9 @@
 
     n_actions = (start_flag +
                  accept_flag +
+                 !!deflect_str +
+                 join_multiparty_flag +
+                 leave_multiparty_flag +
                  hangup_flag +
                  !!dtmf_request);
 
@@ -156,6 +174,7 @@
     mmcli_output_string (MMC_F_CALL_GENERAL_DBUS_PATH,       mm_call_get_path (call));
     mmcli_output_string (MMC_F_CALL_PROPERTIES_NUMBER,       mm_call_get_number (call));
     mmcli_output_string (MMC_F_CALL_PROPERTIES_DIRECTION,    mm_call_direction_get_string (mm_call_get_direction (call)));
+    mmcli_output_string (MMC_F_CALL_PROPERTIES_MULTIPARTY,   mm_call_get_multiparty (call) ? "yes" : "no");
     mmcli_output_string (MMC_F_CALL_PROPERTIES_STATE,        mm_call_state_get_string (mm_call_get_state (call)));
     mmcli_output_string (MMC_F_CALL_PROPERTIES_STATE_REASON, mm_call_state_reason_get_string (mm_call_get_state_reason (call)));
     mmcli_output_string (MMC_F_CALL_PROPERTIES_AUDIO_PORT,   mm_call_get_audio_port (call));
@@ -228,6 +247,87 @@
 }
 
 static void
+deflect_process_reply (gboolean      result,
+                       const GError *error)
+{
+    if (!result) {
+        g_printerr ("error: couldn't deflect the call: '%s'\n",
+                    error ? error->message : "unknown error");
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("successfully deflected the call\n");
+}
+
+static void
+deflect_ready (MMCall        *call,
+               GAsyncResult *result,
+               gpointer      nothing)
+{
+    gboolean operation_result;
+    GError *error = NULL;
+
+    operation_result = mm_call_deflect_finish (call, result, &error);
+    deflect_process_reply (operation_result, error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+join_multiparty_process_reply (gboolean      result,
+                               const GError *error)
+{
+    if (!result) {
+        g_printerr ("error: couldn't join multiparty call: '%s'\n",
+                    error ? error->message : "unknown error");
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("successfully joined multiparty call\n");
+}
+
+static void
+join_multiparty_ready (MMCall        *call,
+                       GAsyncResult *result,
+                       gpointer      nothing)
+{
+    gboolean  operation_result;
+    GError   *error = NULL;
+
+    operation_result = mm_call_join_multiparty_finish (call, result, &error);
+    join_multiparty_process_reply (operation_result, error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+leave_multiparty_process_reply (gboolean      result,
+                                const GError *error)
+{
+    if (!result) {
+        g_printerr ("error: couldn't leave multiparty call: '%s'\n",
+                    error ? error->message : "unknown error");
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("successfully left multiparty call\n");
+}
+
+static void
+leave_multiparty_ready (MMCall        *call,
+                        GAsyncResult *result,
+                        gpointer      nothing)
+{
+    gboolean  operation_result;
+    GError   *error = NULL;
+
+    operation_result = mm_call_leave_multiparty_finish (call, result, &error);
+    leave_multiparty_process_reply (operation_result, error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
 hangup_process_reply (gboolean      result,
                       const GError *error)
 {
@@ -313,6 +413,34 @@
         return;
     }
 
+    /* Requesting to deflect the call? */
+    if (deflect_str) {
+        mm_call_deflect (ctx->call,
+                         deflect_str,
+                         ctx->cancellable,
+                         (GAsyncReadyCallback)deflect_ready,
+                         NULL);
+        return;
+    }
+
+    /* Requesting to join multiparty call? */
+    if (join_multiparty_flag) {
+        mm_call_join_multiparty (ctx->call,
+                                 ctx->cancellable,
+                                 (GAsyncReadyCallback)join_multiparty_ready,
+                                 NULL);
+        return;
+    }
+
+    /* Requesting to leave multiparty call? */
+    if (leave_multiparty_flag) {
+        mm_call_leave_multiparty (ctx->call,
+                                  ctx->cancellable,
+                                  (GAsyncReadyCallback)leave_multiparty_ready,
+                                  NULL);
+        return;
+    }
+
     /* Requesting to hangup the call? */
     if (hangup_flag) {
         mm_call_hangup (ctx->call,
@@ -398,6 +526,40 @@
         return;
     }
 
+    /* Requesting to deflect the call? */
+    if (deflect_str) {
+        gboolean operation_result;
+
+        operation_result = mm_call_deflect_sync (ctx->call,
+                                                 deflect_str,
+                                                 NULL,
+                                                 &error);
+        deflect_process_reply (operation_result, error);
+        return;
+    }
+
+    /* Requesting to join multiparty call? */
+    if (join_multiparty_flag) {
+        gboolean operation_result;
+
+        operation_result = mm_call_join_multiparty_sync (ctx->call,
+                                                         NULL,
+                                                         &error);
+        join_multiparty_process_reply (operation_result, error);
+        return;
+    }
+
+    /* Requesting to leave multiparty call? */
+    if (leave_multiparty_flag) {
+        gboolean operation_result;
+
+        operation_result = mm_call_leave_multiparty_sync (ctx->call,
+                                                          NULL,
+                                                          &error);
+        leave_multiparty_process_reply (operation_result, error);
+        return;
+    }
+
     /* Requesting to hangup the call? */
     if (hangup_flag) {
         gboolean operation_result;
diff --git a/cli/mmcli-common.c b/cli/mmcli-common.c
index 305e91d..c149927 100644
--- a/cli/mmcli-common.c
+++ b/cli/mmcli-common.c
@@ -695,6 +695,7 @@
 
 typedef struct {
     gchar     *sim_path;
+    gchar     *sim_maybe_uid;
     MMManager *manager;
     MMObject  *current;
 } GetSimContext;
@@ -721,6 +722,7 @@
         g_object_unref (ctx->current);
     if (ctx->manager)
         g_object_unref (ctx->manager);
+    g_free (ctx->sim_maybe_uid);
     g_free (ctx->sim_path);
     g_free (ctx);
 }
@@ -800,6 +802,14 @@
 
         object = MM_OBJECT (l->data);
         modem = mm_object_get_modem (object);
+        if (!ctx->sim_path) {
+            if (g_str_equal (ctx->sim_maybe_uid, mm_modem_get_device (modem)))
+                ctx->sim_path = g_strdup (mm_modem_get_sim_path (modem));
+            else {
+                g_object_unref (modem);
+                continue;
+            }
+        }
         if (g_str_equal (ctx->sim_path, mm_modem_get_sim_path (modem))) {
             ctx->current = g_object_ref (object);
             mm_modem_get_sim (modem,
@@ -811,6 +821,12 @@
     }
     g_list_free_full (modems, g_object_unref);
 
+    if (!ctx->sim_path) {
+        g_printerr ("error: invalid index string specified: '%s'\n",
+                    ctx->sim_maybe_uid);
+        exit (EXIT_FAILURE);
+    }
+
     if (!ctx->current) {
         g_printerr ("error: couldn't find sim at '%s'\n",
                     ctx->sim_path);
@@ -821,7 +837,7 @@
 static gchar *
 get_sim_path (const gchar *path_or_index)
 {
-    gchar *sim_path;
+    gchar *sim_path = NULL;
 
     /* We must have a given sim specified */
     if (!path_or_index) {
@@ -837,10 +853,6 @@
     } else if (g_ascii_isdigit (path_or_index[0])) {
         g_debug ("Assuming '%s' is the SIM index", path_or_index);
         sim_path = g_strdup_printf (MM_DBUS_SIM_PREFIX "/%s", path_or_index);
-    } else {
-        g_printerr ("error: invalid index string specified: '%s'\n",
-                    path_or_index);
-        exit (EXIT_FAILURE);
     }
 
     return sim_path;
@@ -848,7 +860,7 @@
 
 void
 mmcli_get_sim (GDBusConnection     *connection,
-               const gchar         *path_or_index,
+               const gchar         *path_or_index_or_uid,
                GCancellable        *cancellable,
                GAsyncReadyCallback  callback,
                gpointer             user_data)
@@ -859,7 +871,8 @@
     task = g_task_new (connection, cancellable, callback, user_data);
 
     ctx = g_new0 (GetSimContext, 1);
-    ctx->sim_path = get_sim_path (path_or_index);
+    ctx->sim_path = get_sim_path (path_or_index_or_uid);
+    ctx->sim_maybe_uid = g_strdup (path_or_index_or_uid);
     g_task_set_task_data (task, ctx, (GDestroyNotify) get_sim_context_free);
 
     mmcli_get_manager (connection,
@@ -870,7 +883,7 @@
 
 MMSim *
 mmcli_get_sim_sync (GDBusConnection  *connection,
-                    const gchar      *path_or_index,
+                    const gchar      *path_or_index_or_uid,
                     MMManager       **o_manager,
                     MMObject        **o_object)
 {
@@ -880,7 +893,7 @@
     MMSim *found = NULL;
     gchar *sim_path;
 
-    sim_path = get_sim_path (path_or_index);
+    sim_path = get_sim_path (path_or_index_or_uid);
 
     manager = mmcli_get_manager_sync (connection);
     modems = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (manager));
@@ -897,6 +910,16 @@
 
         object = MM_OBJECT (l->data);
         modem = mm_object_get_modem (object);
+
+        if (!sim_path) {
+            if (g_str_equal (path_or_index_or_uid, mm_modem_get_device (modem)))
+                sim_path = g_strdup (mm_modem_get_sim_path (modem));
+            else {
+                g_object_unref (modem);
+                continue;
+            }
+        }
+
         if (g_str_equal (sim_path, mm_modem_get_sim_path (modem))) {
             found = mm_modem_get_sim_sync (modem, NULL, &error);
             if (error) {
@@ -914,6 +937,12 @@
         g_object_unref (modem);
     }
 
+    if (!sim_path) {
+        g_printerr ("error: invalid index string specified: '%s'\n",
+                    path_or_index_or_uid);
+        exit (EXIT_FAILURE);
+    }
+
     if (!found) {
         g_printerr ("error: couldn't find sim at '%s'\n", sim_path);
         exit (EXIT_FAILURE);
diff --git a/cli/mmcli-modem-location.c b/cli/mmcli-modem-location.c
index 6e7185c..5469baa 100644
--- a/cli/mmcli-modem-location.c
+++ b/cli/mmcli-modem-location.c
@@ -16,7 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Copyright (C) 2012 Google, Inc.
- * Copyright (C) 2012 Lanedo GmbH <aleksander@lanedo.com>
+ * Copyright (C) 2012 Lanedo GmbH
+ * Copyright (C) 2012-2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include "config.h"
@@ -49,8 +50,10 @@
 static gboolean status_flag;
 static gboolean enable_3gpp_flag;
 static gboolean disable_3gpp_flag;
-static gboolean enable_agps_flag;
-static gboolean disable_agps_flag;
+static gboolean enable_agps_msa_flag;
+static gboolean disable_agps_msa_flag;
+static gboolean enable_agps_msb_flag;
+static gboolean disable_agps_msb_flag;
 static gboolean enable_gps_nmea_flag;
 static gboolean disable_gps_nmea_flag;
 static gboolean enable_gps_raw_flag;
@@ -83,12 +86,20 @@
       "Disable 3GPP location gathering.",
       NULL
     },
-    { "location-enable-agps", 0, 0, G_OPTION_ARG_NONE, &enable_agps_flag,
-      "Enable A-GPS location gathering.",
+    { "location-enable-agps-msa", 0, 0, G_OPTION_ARG_NONE, &enable_agps_msa_flag,
+      "Enable MSA A-GPS location gathering.",
       NULL
     },
-    { "location-disable-agps", 0, 0, G_OPTION_ARG_NONE, &disable_agps_flag,
-      "Disable A-GPS location gathering.",
+    { "location-disable-agps-msa", 0, 0, G_OPTION_ARG_NONE, &disable_agps_msa_flag,
+      "Disable MSA A-GPS location gathering.",
+      NULL
+    },
+    { "location-enable-agps-msb", 0, 0, G_OPTION_ARG_NONE, &enable_agps_msb_flag,
+      "Enable MSB A-GPS location gathering.",
+      NULL
+    },
+    { "location-disable-agps-msb", 0, 0, G_OPTION_ARG_NONE, &disable_agps_msb_flag,
+      "Disable MSB A-GPS location gathering.",
       NULL
     },
     { "location-enable-gps-nmea", 0, 0, G_OPTION_ARG_NONE, &enable_gps_nmea_flag,
@@ -161,6 +172,16 @@
     return group;
 }
 
+#define any_location_setup_flag (                              \
+    enable_3gpp_flag          || disable_3gpp_flag          || \
+    enable_agps_msa_flag      || disable_agps_msa_flag      || \
+    enable_agps_msb_flag      || disable_agps_msb_flag      || \
+    enable_gps_nmea_flag      || disable_gps_nmea_flag      || \
+    enable_gps_raw_flag       || disable_gps_raw_flag       || \
+    enable_cdma_bs_flag       || disable_cdma_bs_flag       || \
+    enable_gps_unmanaged_flag || disable_gps_unmanaged_flag || \
+    set_enable_signal_flag    || set_disable_signal_flag)
+
 gboolean
 mmcli_modem_location_options_enabled (void)
 {
@@ -170,12 +191,13 @@
     if (checked)
         return !!n_actions;
 
-    if ((enable_3gpp_flag && disable_3gpp_flag) ||
-        (enable_agps_flag && disable_agps_flag) ||
-        (enable_gps_nmea_flag && disable_gps_nmea_flag) ||
-        (enable_gps_raw_flag && disable_gps_raw_flag) ||
+    if ((enable_3gpp_flag          && disable_3gpp_flag)          ||
+        (enable_agps_msa_flag      && disable_agps_msa_flag)      ||
+        (enable_agps_msb_flag      && disable_agps_msb_flag)      ||
+        (enable_gps_nmea_flag      && disable_gps_nmea_flag)      ||
+        (enable_gps_raw_flag       && disable_gps_raw_flag)       ||
         (enable_gps_unmanaged_flag && disable_gps_unmanaged_flag) ||
-        (enable_cdma_bs_flag && disable_cdma_bs_flag)) {
+        (enable_cdma_bs_flag       && disable_cdma_bs_flag)) {
         g_printerr ("error: cannot enable and disable the same source\n");
         exit (EXIT_FAILURE);
     }
@@ -186,20 +208,7 @@
     }
 
     n_actions = (status_flag +
-                 !!(enable_3gpp_flag +
-                    disable_3gpp_flag +
-                    enable_agps_flag +
-                    disable_agps_flag +
-                    enable_gps_nmea_flag +
-                    disable_gps_nmea_flag +
-                    enable_gps_raw_flag +
-                    disable_gps_raw_flag +
-                    enable_cdma_bs_flag +
-                    disable_cdma_bs_flag +
-                    enable_gps_unmanaged_flag +
-                    disable_gps_unmanaged_flag +
-                    set_enable_signal_flag +
-                    set_disable_signal_flag) +
+                 any_location_setup_flag +
                  get_flag +
                  !!set_supl_server_str +
                  !!inject_assistance_data_str +
@@ -280,7 +289,7 @@
         gps_refresh_rate = g_strdup_printf ("%u", rate);
 
         /* If A-GPS supported, show SUPL server setup */
-        if (mm_modem_location_get_capabilities (ctx->modem_location) & MM_MODEM_LOCATION_SOURCE_AGPS)
+        if (mm_modem_location_get_capabilities (ctx->modem_location) & (MM_MODEM_LOCATION_SOURCE_AGPS_MSA | MM_MODEM_LOCATION_SOURCE_AGPS_MSB))
             gps_supl_server = mm_modem_location_get_supl_server (ctx->modem_location);
 
         mask = mm_modem_location_get_supported_assistance_data (ctx->modem_location);
@@ -451,10 +460,15 @@
     if (disable_3gpp_flag)
         sources &= ~MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI;
 
-    if (enable_agps_flag)
-        sources |= MM_MODEM_LOCATION_SOURCE_AGPS;
-    if (disable_agps_flag)
-        sources &= ~MM_MODEM_LOCATION_SOURCE_AGPS;
+    if (enable_agps_msa_flag)
+        sources |= MM_MODEM_LOCATION_SOURCE_AGPS_MSA;
+    if (disable_agps_msa_flag)
+        sources &= ~MM_MODEM_LOCATION_SOURCE_AGPS_MSA;
+
+    if (enable_agps_msb_flag)
+        sources |= MM_MODEM_LOCATION_SOURCE_AGPS_MSB;
+    if (disable_agps_msb_flag)
+        sources &= ~MM_MODEM_LOCATION_SOURCE_AGPS_MSB;
 
     if (enable_gps_nmea_flag)
         sources |= MM_MODEM_LOCATION_SOURCE_GPS_NMEA;
@@ -597,20 +611,7 @@
         g_assert_not_reached ();
 
     /* Request to setup location gathering? */
-    if (enable_3gpp_flag ||
-        disable_3gpp_flag ||
-        enable_agps_flag ||
-        disable_agps_flag ||
-        enable_gps_nmea_flag ||
-        disable_gps_nmea_flag ||
-        enable_gps_raw_flag ||
-        disable_gps_raw_flag ||
-        enable_cdma_bs_flag ||
-        disable_cdma_bs_flag ||
-        enable_gps_unmanaged_flag ||
-        disable_gps_unmanaged_flag ||
-        set_enable_signal_flag ||
-        set_disable_signal_flag) {
+    if (any_location_setup_flag) {
         g_debug ("Asynchronously setting up location gathering...");
         mm_modem_location_setup (ctx->modem_location,
                                  build_sources_from_flags (),
@@ -727,20 +728,7 @@
     }
 
     /* Request to setup location gathering? */
-    if (enable_3gpp_flag ||
-        disable_3gpp_flag ||
-        enable_agps_flag ||
-        disable_agps_flag ||
-        enable_gps_nmea_flag ||
-        disable_gps_nmea_flag ||
-        enable_gps_raw_flag ||
-        disable_gps_raw_flag ||
-        enable_cdma_bs_flag ||
-        disable_cdma_bs_flag ||
-        enable_gps_unmanaged_flag ||
-        disable_gps_unmanaged_flag ||
-        set_enable_signal_flag ||
-        set_disable_signal_flag) {
+    if (any_location_setup_flag) {
         gboolean result;
 
         g_debug ("Synchronously setting up location gathering...");
diff --git a/cli/mmcli-modem-voice.c b/cli/mmcli-modem-voice.c
index 089523e..87c27be 100644
--- a/cli/mmcli-modem-voice.c
+++ b/cli/mmcli-modem-voice.c
@@ -50,6 +50,13 @@
 static gboolean list_flag;
 static gchar *create_str;
 static gchar *delete_str;
+static gboolean hold_and_accept_flag;
+static gboolean hangup_and_accept_flag;
+static gboolean hangup_all_flag;
+static gboolean transfer_flag;
+static gboolean call_waiting_enable_flag;
+static gboolean call_waiting_disable_flag;
+static gboolean call_waiting_query_flag;
 
 static GOptionEntry entries[] = {
     { "voice-list-calls", 0, 0, G_OPTION_ARG_NONE, &list_flag,
@@ -64,6 +71,34 @@
       "Delete a call from a given modem",
       "[PATH|INDEX]"
     },
+    { "voice-hold-and-accept", 0, 0, G_OPTION_ARG_NONE, &hold_and_accept_flag,
+      "Places all active calls in hold and accepts the next waiting or held call",
+      NULL
+    },
+    { "voice-hangup-and-accept", 0, 0, G_OPTION_ARG_NONE, &hangup_and_accept_flag,
+      "Hangs up all active calls and accepts the next waiting or held call",
+      NULL
+    },
+    { "voice-hangup-all", 0, 0, G_OPTION_ARG_NONE, &hangup_all_flag,
+      "Hangs up all ongoing (active, waiting, held) calls",
+      NULL
+    },
+    { "voice-transfer", 0, 0, G_OPTION_ARG_NONE, &transfer_flag,
+      "Joins active and held calls and disconnects from them",
+      NULL
+    },
+    { "voice-enable-call-waiting", 0, 0, G_OPTION_ARG_NONE, &call_waiting_enable_flag,
+      "Enables the call waiting network service",
+      NULL
+    },
+    { "voice-disable-call-waiting", 0, 0, G_OPTION_ARG_NONE, &call_waiting_disable_flag,
+      "Disables the call waiting network service",
+      NULL
+    },
+    { "voice-query-call-waiting", 0, 0, G_OPTION_ARG_NONE, &call_waiting_query_flag,
+      "Queries the status of the call waiting network service",
+      NULL
+    },
     { NULL }
 };
 
@@ -93,7 +128,14 @@
 
     n_actions = (list_flag +
                  !!create_str +
-                 !!delete_str);
+                 !!delete_str +
+                 hold_and_accept_flag +
+                 hangup_and_accept_flag +
+                 hangup_all_flag +
+                 transfer_flag +
+                 call_waiting_enable_flag +
+                 call_waiting_disable_flag +
+                 call_waiting_query_flag);
 
     if (n_actions > 1) {
         g_printerr ("error: too many Voice actions requested\n");
@@ -176,6 +218,158 @@
 }
 
 static void
+call_waiting_query_process_reply (const GError *error,
+                                  gboolean      status)
+{
+    if (error) {
+        g_printerr ("error: couldn't query call waiting network service status: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("call waiting service is %s\n", status ? "enabled" : "disabled");
+}
+
+static void
+call_waiting_query_ready (MMModemVoice *modem,
+                          GAsyncResult *result,
+                          gpointer     nothing)
+{
+    GError   *error = NULL;
+    gboolean  status = FALSE;
+
+    mm_modem_voice_call_waiting_query_finish (modem, result, &status, &error);
+    call_waiting_query_process_reply (error, status);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+call_waiting_setup_process_reply (const GError *error)
+{
+    if (error) {
+        g_printerr ("error: couldn't setup call waiting network service: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("operation successful\n");
+}
+
+static void
+call_waiting_setup_ready (MMModemVoice *modem,
+                          GAsyncResult *result,
+                          gpointer     nothing)
+{
+    GError *error = NULL;
+
+    mm_modem_voice_call_waiting_setup_finish (modem, result, &error);
+    call_waiting_setup_process_reply (error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+transfer_process_reply (const GError *error)
+{
+    if (error) {
+        g_printerr ("error: couldn't hangup all: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("operation successful\n");
+}
+
+static void
+transfer_ready (MMModemVoice *modem,
+                GAsyncResult *result,
+                gpointer     nothing)
+{
+    GError *error = NULL;
+
+    mm_modem_voice_transfer_finish (modem, result, &error);
+    transfer_process_reply (error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+hangup_all_process_reply (const GError *error)
+{
+    if (error) {
+        g_printerr ("error: couldn't hangup all: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("operation successful\n");
+}
+
+static void
+hangup_all_ready (MMModemVoice *modem,
+                  GAsyncResult *result,
+                  gpointer     nothing)
+{
+    GError *error = NULL;
+
+    mm_modem_voice_hangup_all_finish (modem, result, &error);
+    hangup_all_process_reply (error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+hangup_and_accept_process_reply (const GError *error)
+{
+    if (error) {
+        g_printerr ("error: couldn't hangup and accept: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("operation successful\n");
+}
+
+static void
+hangup_and_accept_ready (MMModemVoice *modem,
+                         GAsyncResult *result,
+                         gpointer     nothing)
+{
+    GError *error = NULL;
+
+    mm_modem_voice_hangup_and_accept_finish (modem, result, &error);
+    hangup_and_accept_process_reply (error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
+hold_and_accept_process_reply (const GError *error)
+{
+    if (error) {
+        g_printerr ("error: couldn't hold and accept: '%s'\n",
+                    error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    g_print ("operation successful\n");
+}
+
+static void
+hold_and_accept_ready (MMModemVoice *modem,
+                         GAsyncResult *result,
+                         gpointer     nothing)
+{
+    GError *error = NULL;
+
+    mm_modem_voice_hold_and_accept_finish (modem, result, &error);
+    hold_and_accept_process_reply (error);
+
+    mmcli_async_operation_done ();
+}
+
+static void
 list_process_reply (GList        *result,
                     const GError *error)
 {
@@ -334,6 +528,78 @@
         return;
     }
 
+    /* Request to hold and accept? */
+    if (hold_and_accept_flag) {
+        g_debug ("Asynchronously holding and accepting next call...");
+        mm_modem_voice_hold_and_accept (ctx->modem_voice,
+                                          ctx->cancellable,
+                                          (GAsyncReadyCallback)hold_and_accept_ready,
+                                          NULL);
+        return;
+    }
+
+    /* Request to hangup and accept? */
+    if (hangup_and_accept_flag) {
+        g_debug ("Asynchronously hanging up and accepting next call...");
+        mm_modem_voice_hangup_and_accept (ctx->modem_voice,
+                                          ctx->cancellable,
+                                          (GAsyncReadyCallback)hangup_and_accept_ready,
+                                          NULL);
+        return;
+    }
+
+    /* Request to hangup all? */
+    if (hangup_all_flag) {
+        g_debug ("Asynchronously hanging up all calls...");
+        mm_modem_voice_hangup_all (ctx->modem_voice,
+                                   ctx->cancellable,
+                                   (GAsyncReadyCallback)hangup_all_ready,
+                                   NULL);
+        return;
+    }
+
+    /* Request to transfer? */
+    if (transfer_flag) {
+        g_debug ("Asynchronously transferring calls...");
+        mm_modem_voice_transfer (ctx->modem_voice,
+                                 ctx->cancellable,
+                                 (GAsyncReadyCallback)transfer_ready,
+                                 NULL);
+        return;
+    }
+
+    /* Request to enable call waiting? */
+    if (call_waiting_enable_flag) {
+        g_debug ("Asynchronously enabling call waiting...");
+        mm_modem_voice_call_waiting_setup (ctx->modem_voice,
+                                           TRUE,
+                                           ctx->cancellable,
+                                           (GAsyncReadyCallback)call_waiting_setup_ready,
+                                           NULL);
+        return;
+    }
+
+    /* Request to disable call waiting? */
+    if (call_waiting_disable_flag) {
+        g_debug ("Asynchronously enabling call waiting...");
+        mm_modem_voice_call_waiting_setup (ctx->modem_voice,
+                                           FALSE,
+                                           ctx->cancellable,
+                                           (GAsyncReadyCallback)call_waiting_setup_ready,
+                                           NULL);
+        return;
+    }
+
+    /* Request to query call waiting? */
+    if (call_waiting_query_flag) {
+        g_debug ("Asynchronously querying call waiting status...");
+        mm_modem_voice_call_waiting_query (ctx->modem_voice,
+                                           ctx->cancellable,
+                                           (GAsyncReadyCallback)call_waiting_query_ready,
+                                           NULL);
+        return;
+    }
+
     g_warn_if_reached ();
 }
 
@@ -430,5 +696,63 @@
         return;
     }
 
+    /* Request to hold and accept? */
+    if (hold_and_accept_flag) {
+        g_debug ("Synchronously holding and accepting call...");
+        mm_modem_voice_hold_and_accept_sync (ctx->modem_voice, NULL, &error);
+        hold_and_accept_process_reply (error);
+        return;
+    }
+
+    /* Request to hangup and accept? */
+    if (hangup_and_accept_flag) {
+        g_debug ("Synchronously hanging up and accepting call...");
+        mm_modem_voice_hangup_and_accept_sync (ctx->modem_voice, NULL, &error);
+        hangup_and_accept_process_reply (error);
+        return;
+    }
+
+    /* Request to hangup all? */
+    if (hangup_all_flag) {
+        g_debug ("Synchronously hanging up all calls...");
+        mm_modem_voice_hangup_all_sync (ctx->modem_voice, NULL, &error);
+        hangup_all_process_reply (error);
+        return;
+    }
+
+    /* Request to transfer? */
+    if (transfer_flag) {
+        g_debug ("Synchronously transferring calls...");
+        mm_modem_voice_transfer_sync (ctx->modem_voice, NULL, &error);
+        transfer_process_reply (error);
+        return;
+    }
+
+    /* Request to enable call waiting? */
+    if (call_waiting_enable_flag) {
+        g_debug ("Synchronously enabling call waiting...");
+        mm_modem_voice_call_waiting_setup_sync (ctx->modem_voice, TRUE, NULL, &error);
+        call_waiting_setup_process_reply (error);
+        return;
+    }
+
+    /* Request to disable call waiting? */
+    if (call_waiting_disable_flag) {
+        g_debug ("Synchronously disabling call waiting...");
+        mm_modem_voice_call_waiting_setup_sync (ctx->modem_voice, FALSE, NULL, &error);
+        call_waiting_setup_process_reply (error);
+        return;
+    }
+
+    /* Request to query call waiting? */
+    if (call_waiting_query_flag) {
+        gboolean status = FALSE;
+
+        g_debug ("Synchronously querying call waiting status...");
+        mm_modem_voice_call_waiting_query_sync (ctx->modem_voice, NULL, &status, &error);
+        call_waiting_query_process_reply (error, status);
+        return;
+    }
+
     g_warn_if_reached ();
 }
diff --git a/cli/mmcli-output.c b/cli/mmcli-output.c
index 0242460..4fd9db3 100644
--- a/cli/mmcli-output.c
+++ b/cli/mmcli-output.c
@@ -227,6 +227,7 @@
     [MMC_F_CALL_GENERAL_DBUS_PATH]            = { "call.dbus-path",                                  "dbus path",                MMC_S_CALL_GENERAL,            },
     [MMC_F_CALL_PROPERTIES_NUMBER]            = { "call.properties.number",                          "number",                   MMC_S_CALL_PROPERTIES,         },
     [MMC_F_CALL_PROPERTIES_DIRECTION]         = { "call.properties.direction",                       "direction",                MMC_S_CALL_PROPERTIES,         },
+    [MMC_F_CALL_PROPERTIES_MULTIPARTY]        = { "call.properties.multiparty",                      "multiparty",               MMC_S_CALL_PROPERTIES,         },
     [MMC_F_CALL_PROPERTIES_STATE]             = { "call.properties.state",                           "state",                    MMC_S_CALL_PROPERTIES,         },
     [MMC_F_CALL_PROPERTIES_STATE_REASON]      = { "call.properties.state-reason",                    "state reason",             MMC_S_CALL_PROPERTIES,         },
     [MMC_F_CALL_PROPERTIES_AUDIO_PORT]        = { "call.properties.audio-port",                      "audio port",               MMC_S_CALL_PROPERTIES,         },
@@ -1071,6 +1072,129 @@
 }
 
 /******************************************************************************/
+/* JSON-friendly output */
+
+static gint
+list_sort_by_keys (const OutputItem *item_a,
+                   const OutputItem *item_b)
+{
+    return g_strcmp0 (field_infos[item_a->field].key, field_infos[item_b->field].key);
+}
+
+static void
+dump_output_json (void)
+{
+    GList   *l;
+    MmcF     current_field = MMC_F_UNKNOWN;
+    gchar  **current_path = NULL;
+    guint    cur_dlen = 0;
+
+    output_items = g_list_sort (output_items, (GCompareFunc) list_sort_by_keys);
+
+    g_print("{");
+    for (l = output_items; l; l = g_list_next (l)) {
+        OutputItem *item_l = (OutputItem *)(l->data);
+
+        if (current_field != item_l->field) {
+            guint   new_dlen;
+            guint   iter = 0;
+            gchar **new_path;
+
+            new_path = g_strsplit (field_infos[item_l->field].key, ".", -1);
+            new_dlen = g_strv_length (new_path) - 1;
+            if (current_path) {
+                guint min_dlen;
+
+                min_dlen = MIN (cur_dlen, new_dlen);
+                while (iter < min_dlen && g_strcmp0 (current_path[iter], new_path[iter]) == 0)
+                    iter++;
+
+                g_strfreev (current_path);
+
+                if (iter < min_dlen || new_dlen < cur_dlen)
+                    for (min_dlen = iter; min_dlen < cur_dlen; min_dlen++)
+                        g_print ("}");
+
+                g_print (",");
+            }
+
+            while (iter < new_dlen)
+                g_print ("\"%s\":{", new_path[iter++]);
+
+            cur_dlen = new_dlen;
+            current_path = new_path;
+            current_field = item_l->field;
+        } else {
+            g_print (",");
+        }
+
+        if (item_l->type == VALUE_TYPE_SINGLE) {
+            OutputItemSingle *single = (OutputItemSingle *) item_l;
+            gchar            *escaped = NULL;
+
+            if (single->value)
+                escaped = g_strescape (single->value, "\v");
+
+            g_print ("\"%s\":\"%s\"", current_path[cur_dlen], escaped ? escaped : "--");
+            g_free (escaped);
+        } else if (item_l->type == VALUE_TYPE_MULTIPLE) {
+            OutputItemMultiple *multiple = (OutputItemMultiple *) item_l;
+            guint               i, n;
+
+            n = multiple->values ? g_strv_length (multiple->values) : 0;
+
+            g_print ("\"%s\":[", current_path[cur_dlen]);
+            for (i = 0; i < n; i++) {
+                gchar *escaped;
+
+                escaped = g_strescape (multiple->values[i], "\v");
+                g_print("\"%s\"", escaped);
+                if (i < n - 1)
+                    g_print(",");
+                g_free (escaped);
+            }
+            g_print("]");
+        } else
+            g_assert_not_reached ();
+    }
+
+    while (cur_dlen--)
+        g_print ("}");
+    g_print("}\n");
+
+    g_strfreev (current_path);
+}
+
+static void
+dump_output_list_json (MmcF field)
+{
+    GList *l;
+
+    g_assert (field != MMC_F_UNKNOWN);
+
+    g_print("{\"%s\":[", field_infos[field].key);
+
+    for (l = output_items; l; l = g_list_next (l)) {
+        OutputItem         *item_l;
+        OutputItemListitem *listitem;
+
+        item_l = (OutputItem *)(l->data);
+        g_assert (item_l->type == VALUE_TYPE_LISTITEM);
+        listitem = (OutputItemListitem *)item_l;
+        g_assert (listitem->value);
+
+        /* All items must be of same type */
+        g_assert_cmpint (item_l->field, ==, field);
+        g_print("\"%s\"", listitem->value);
+
+        if (g_list_next (l))
+            g_print(",");
+    }
+
+    g_print("]}\n");
+}
+
+/******************************************************************************/
 /* Dump output */
 
 void
@@ -1085,6 +1209,9 @@
     case MMC_OUTPUT_TYPE_KEYVALUE:
         dump_output_keyvalue ();
         break;
+    case MMC_OUTPUT_TYPE_JSON:
+        dump_output_json ();
+        break;
     }
 
     g_list_free_full (output_items, (GDestroyNotify) output_item_free);
@@ -1105,6 +1232,9 @@
     case MMC_OUTPUT_TYPE_KEYVALUE:
         dump_output_list_keyvalue (field);
         break;
+    case MMC_OUTPUT_TYPE_JSON:
+        dump_output_list_json (field);
+        break;
     }
 
     g_list_free_full (output_items, (GDestroyNotify) output_item_free);
diff --git a/cli/mmcli-output.h b/cli/mmcli-output.h
index 7793f79..135d429 100644
--- a/cli/mmcli-output.h
+++ b/cli/mmcli-output.h
@@ -243,6 +243,7 @@
     MMC_F_CALL_GENERAL_DBUS_PATH,
     MMC_F_CALL_PROPERTIES_NUMBER,
     MMC_F_CALL_PROPERTIES_DIRECTION,
+    MMC_F_CALL_PROPERTIES_MULTIPARTY,
     MMC_F_CALL_PROPERTIES_STATE,
     MMC_F_CALL_PROPERTIES_STATE_REASON,
     MMC_F_CALL_PROPERTIES_AUDIO_PORT,
@@ -284,6 +285,7 @@
     MMC_OUTPUT_TYPE_NONE,
     MMC_OUTPUT_TYPE_HUMAN,
     MMC_OUTPUT_TYPE_KEYVALUE,
+    MMC_OUTPUT_TYPE_JSON
 } MmcOutputType;
 
 void          mmcli_output_set (MmcOutputType type);
diff --git a/cli/mmcli.c b/cli/mmcli.c
index 6953672..af41f0a 100644
--- a/cli/mmcli.c
+++ b/cli/mmcli.c
@@ -45,6 +45,7 @@
 
 /* Context */
 static gboolean output_keyvalue_flag;
+static gboolean output_json_flag;
 static gboolean verbose_flag;
 static gboolean version_flag;
 static gboolean async_flag;
@@ -55,6 +56,10 @@
       "Run action with machine-friendly key-value output",
       NULL
     },
+    { "output-json", 'J', 0, G_OPTION_ARG_NONE, &output_json_flag,
+      "Run action with machine-friendly json output",
+      NULL
+    },
     { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose_flag,
       "Run action with verbose logs",
       NULL
@@ -237,14 +242,26 @@
         g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MASK, log_handler, NULL);
 
     /* Setup output */
+    if (output_keyvalue_flag && output_json_flag) {
+        g_printerr ("error: only one output type supported at the same time\n");
+        exit (EXIT_FAILURE);
+    }
     if (output_keyvalue_flag) {
         if (verbose_flag) {
             g_printerr ("error: cannot set verbose output in keyvalue output type\n");
             exit (EXIT_FAILURE);
         }
         mmcli_output_set (MMC_OUTPUT_TYPE_KEYVALUE);
-    } else
+    }
+    else if (output_json_flag) {
+        if (verbose_flag) {
+            g_printerr ("error: cannot set verbose output in JSON output type\n");
+            exit (EXIT_FAILURE);
+        }
+        mmcli_output_set (MMC_OUTPUT_TYPE_JSON);
+    } else {
         mmcli_output_set (MMC_OUTPUT_TYPE_HUMAN);
+    }
 
     /* Setup signals */
     signal (SIGINT, signals_handler);
diff --git a/configure.ac b/configure.ac
index a6ebfd6..d3c9beb 100644
--- a/configure.ac
+++ b/configure.ac
@@ -314,6 +314,23 @@
 AM_CONDITIONAL(WITH_POLKIT, [test "x$with_polkit" != "xno"])
 
 dnl-----------------------------------------------------------------------------
+dnl AT command via DBus support (disabled by default unless running in --debug)
+dnl
+dnl It is suggested that this option is only enabled in custom built systems and
+dnl only if truly required.
+dnl
+
+AC_ARG_WITH(at_command_via_dbus,
+            AS_HELP_STRING([--with-at-command-via-dbus],
+                           [Build with Modem.Command() interface enabled always]),
+            [],
+            [with_at_command_via_dbus=no])
+
+if test "x$with_at_command_via_dbus" = "xyes"; then
+    AC_DEFINE(WITH_AT_COMMAND_VIA_DBUS, 1, [Define if you want to enable AT commands via DBus])
+fi
+
+dnl-----------------------------------------------------------------------------
 dnl MBIM support (enabled by default)
 dnl
 
@@ -444,6 +461,7 @@
       qmi:                     ${with_qmi}
       systemd suspend/resume:  ${with_systemd_suspend_resume}
       systemd journal:         ${with_systemd_journal}
+      at command via dbus:     ${with_at_command_via_dbus}
 
     Miscellaneous:
       gobject introspection:   ${found_introspection}
diff --git a/decode/packet.py b/decode/packet.py
index e28b62f..97daee1 100644
--- a/decode/packet.py
+++ b/decode/packet.py
@@ -109,7 +109,7 @@
     def add_line(self, line):
         line = line.strip()
         if not len(line):
-        	return
+            return
         self.lines.append(line)
 
         if line[0] == '[':
diff --git a/docs/man/mmcli.1 b/docs/man/mmcli.1
index cb00f32..4a08792 100644
--- a/docs/man/mmcli.1
+++ b/docs/man/mmcli.1
@@ -390,16 +390,26 @@
 .B \-\-location\-disable\-3gpp
 Disable location discovery using the 3GPP network.
 .TP
-.B \-\-location\-enable\-agps
+.B \-\-location\-enable\-agps-msa
 Enable A-GPS (MSA) support. This command does not implicitly start the GPS
 engine, it just specifies that A-GPS should be enabled when the engine is
 started. Therefore, the user should request enabling A-GPS before the raw
 or NMEA outputs are enabled with \fB\-\-location\-enable\-gps\-raw\fR or
 \fB\-\-location\-enable\-gps\-nmea\fR.
 .TP
-.B \-\-location\-disable\-agps
+.B \-\-location\-disable\-agps-msa
 Disable A-GPS (MSA) support.
 .TP
+.B \-\-location\-enable\-agps-msb
+Enable A-GPS (MSB) support. This command does not implicitly start the GPS
+engine, it just specifies that A-GPS should be enabled when the engine is
+started. Therefore, the user should request enabling A-GPS before the raw
+or NMEA outputs are enabled with \fB\-\-location\-enable\-gps\-raw\fR or
+\fB\-\-location\-enable\-gps\-nmea\fR.
+.TP
+.B \-\-location\-disable\-agps-msb
+Disable A-GPS (MSB) support.
+.TP
 .B \-\-location\-enable\-gps\-nmea
 Enable location discovery using GPS and reported with NMEA traces.
 
@@ -893,14 +903,14 @@
 .Bd -literal -compact
     $ mmcli -m 0 --location-status
       --------------------------------
-      Location |      capabilities: 3gpp-lac-ci, gps-raw, gps-nmea, agps
+      Location |      capabilities: 3gpp-lac-ci, gps-raw, gps-nmea, agps-msa, agps-msb
                |           enabled: 3gpp-lac-ci
                |           signals: no
       -----------------------------
       GPS      |      refresh rate: 30 seconds
                | a-gps supl server: supl.google.com:7276
 
-    $ sudo mmcli -m 0 --location-enable-agps
+    $ sudo mmcli -m 0 --location-enable-agps-msa
     successfully setup location gathering
 
     $ sudo mmcli -m 0 --location-enable-gps-nmea
@@ -924,7 +934,7 @@
 .Bd -literal -compact
     $ mmcli -m 0 --location-status
       --------------------------------
-      Location |         capabilities: 3gpp-lac-ci, gps-raw, gps-nmea, agps
+      Location |         capabilities: 3gpp-lac-ci, gps-raw, gps-nmea, agps-msa, agps-msb
                |              enabled: 3gpp-lac-ci
                |              signals: no
       --------------------------------
diff --git a/docs/reference/api/ModemManager-docs.xml b/docs/reference/api/ModemManager-docs.xml
index 93d6e95..2918af6 100644
--- a/docs/reference/api/ModemManager-docs.xml
+++ b/docs/reference/api/ModemManager-docs.xml
@@ -19,22 +19,22 @@
 
     <authorgroup>
       <author>
-	    <firstname>Dan</firstname>
-	    <surname>Williams</surname>
-	    <affiliation>
-	      <address>
-	        <email>dcbw@redhat.com</email>
-	      </address>
-	    </affiliation>
+        <firstname>Dan</firstname>
+        <surname>Williams</surname>
+        <affiliation>
+          <address>
+            <email>dcbw@redhat.com</email>
+          </address>
+        </affiliation>
       </author>
       <author>
-	    <firstname>Aleksander</firstname>
-	    <surname>Morgado</surname>
-	    <affiliation>
-	      <address>
-	        <email>aleksander@aleksander.es</email>
-	      </address>
-	    </affiliation>
+        <firstname>Aleksander</firstname>
+        <surname>Morgado</surname>
+        <affiliation>
+          <address>
+            <email>aleksander@aleksander.es</email>
+          </address>
+        </affiliation>
       </author>
     </authorgroup>
 
diff --git a/docs/reference/api/ModemManager-overview.xml b/docs/reference/api/ModemManager-overview.xml
index 963c0df..80c1805 100644
--- a/docs/reference/api/ModemManager-overview.xml
+++ b/docs/reference/api/ModemManager-overview.xml
@@ -125,6 +125,24 @@
             </programlisting>
           </listitem>
           <listitem>
+            <para><emphasis>MM_FILTER_RULE_EXPLICIT_BLACKLIST</emphasis></para>
+            <para>
+              This filter allows users to manually tag devices and/or device ports with the
+              <emphasis>ID_MM_DEVICE_IGNORE</emphasis> udev tag. If the filter finds this tag,
+              the device and/or device ports will be automatically ignored and port probing
+              will be never run on them.
+            </para>
+            <programlisting>
+$ sudo vim /lib/udev/rules.d/78-mm-blacklist-internal-modem.rules
+    ACTION!="add|change|move", GOTO="mm_blacklist_internal_modem_end"
+    ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{ID_MM_DEVICE_IGNORE}="1"
+    LABEL="mm_blacklist_internal_modem_end"
+// Apply new rules without reboot
+$ sudo udevadm control --reload
+$ sudo udevadm trigger
+            </programlisting>
+          </listitem>
+          <listitem>
             <para><emphasis>MM_FILTER_RULE_VIRTUAL</emphasis></para>
             <para>
               This filter will automatically flag as forbidden all ports exposed by virtual
@@ -167,7 +185,7 @@
             <para><emphasis>MM_FILTER_RULE_TTY_BLACKLIST</emphasis></para>
             <para>
               This filter will not allow probing any of the devices flagged as
-              <emphasis>ID_MM_DEVICE_IGNORE</emphasis>, like the ones in the default blacklist
+              <emphasis>ID_MM_TTY_BLACKLIST</emphasis>, like the ones in the default blacklist
               shipped by ModemManager.
             </para>
           </listitem>
@@ -175,7 +193,7 @@
             <para><emphasis>MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY</emphasis></para>
             <para>
               This filter will not allow automatic probing any of the devices flagged as
-              <emphasis>ID_MM_DEVICE_MANUAL_SCAN_ONLY</emphasis>, like the ones in the default
+              <emphasis>ID_MM_TTY_MANUAL_SCAN_ONLY</emphasis>, like the ones in the default
               USB serial adapters greylist shipped by ModemManager. Devices flagged like
               this will only be probed when a manual scan is requested via the
               <link linkend="gdbus-method-org-freedesktop-ModemManager1.ScanDevices">ScanDevices</link>
@@ -185,13 +203,7 @@
           <listitem>
             <para><emphasis>MM_FILTER_RULE_TTY_PLATFORM_DRIVER</emphasis></para>
             <para>
-              If this filter is enabled, all platform TTY ports not explicitly flagged with the
-              <emphasis>ID_MM_PLATFORM_DRIVER_PROBE</emphasis> will be forbidden. If the flag
-              is found in a platform TTY port, port probing will be allowed directly.
-            </para>
-            <para>
-              Note that this filter is obsoleted by the more generic MM_FILTER_RULE_EXPLICIT_WHITELIST
-              filter. It is maintained for backwards compatibility with older ModemManager versions.
+              If this filter is enabled, all platform TTY ports will be forbidden automatically.
             </para>
           </listitem>
           <listitem>
@@ -239,6 +251,7 @@
               This is a policy where the following rules are enabled:
               <itemizedlist>
                 <listitem>MM_FILTER_RULE_EXPLICIT_WHITELIST</listitem>
+                <listitem>MM_FILTER_RULE_EXPLICIT_BLACKLIST</listitem>
                 <listitem>MM_FILTER_RULE_VIRTUAL</listitem>
                 <listitem>MM_FILTER_RULE_NET</listitem>
                 <listitem>MM_FILTER_RULE_CDC_WDM</listitem>
@@ -264,6 +277,7 @@
               This is a policy where the following rules are enabled:
               <itemizedlist>
                 <listitem>MM_FILTER_RULE_EXPLICIT_WHITELIST</listitem>
+                <listitem>MM_FILTER_RULE_EXPLICIT_BLACKLIST</listitem>
                 <listitem>MM_FILTER_RULE_VIRTUAL</listitem>
                 <listitem>MM_FILTER_RULE_NET</listitem>
                 <listitem>MM_FILTER_RULE_CDC_WDM</listitem>
@@ -290,6 +304,7 @@
               This policy is a mix of the Default and Strict ones:
               <itemizedlist>
                 <listitem>MM_FILTER_RULE_EXPLICIT_WHITELIST</listitem>
+                <listitem>MM_FILTER_RULE_EXPLICIT_BLACKLIST</listitem>
                 <listitem>MM_FILTER_RULE_VIRTUAL</listitem>
                 <listitem>MM_FILTER_RULE_NET</listitem>
                 <listitem>MM_FILTER_RULE_CDC_WDM</listitem>
diff --git a/docs/reference/api/ModemManager-sections.txt b/docs/reference/api/ModemManager-sections.txt
index 5fab5c6..15d976c 100644
--- a/docs/reference/api/ModemManager-sections.txt
+++ b/docs/reference/api/ModemManager-sections.txt
@@ -139,8 +139,10 @@
 MM_MODEM_BAND_CDMA_BC17_US_FLO_2500
 MM_MODEM_BAND_CDMA_BC18_US_PS_700
 MM_MODEM_BAND_CDMA_BC19_US_LOWER_700
+MM_MODEM_LOCATION_SOURCE_AGPS
 <SUBSECTION Private>
 MMModemBandDeprecated
+MMModemLocationSourceDeprecated
 MM_DEPRECATED
 </SECTION>
 
@@ -149,11 +151,11 @@
 <TITLE>Common udev tags</TITLE>
 ID_MM_CANDIDATE
 ID_MM_PHYSDEV_UID
-ID_MM_PORT_IGNORE
 ID_MM_DEVICE_PROCESS
 ID_MM_DEVICE_IGNORE
-ID_MM_DEVICE_MANUAL_SCAN_ONLY
-ID_MM_PLATFORM_DRIVER_PROBE
+ID_MM_PORT_IGNORE
+ID_MM_TTY_BLACKLIST
+ID_MM_TTY_MANUAL_SCAN_ONLY
 ID_MM_PORT_TYPE_AT_PPP
 ID_MM_PORT_TYPE_AT_PRIMARY
 ID_MM_PORT_TYPE_AT_SECONDARY
diff --git a/docs/reference/libmm-glib/libmm-glib-docs.xml b/docs/reference/libmm-glib/libmm-glib-docs.xml
index 5778936..ec4d4c9 100644
--- a/docs/reference/libmm-glib/libmm-glib-docs.xml
+++ b/docs/reference/libmm-glib/libmm-glib-docs.xml
@@ -19,13 +19,13 @@
 
     <authorgroup>
       <author>
-	    <firstname>Aleksander</firstname>
-	    <surname>Morgado</surname>
-	    <affiliation>
-	      <address>
-	        <email>aleksander@aleksander.es</email>
-	      </address>
-	    </affiliation>
+        <firstname>Aleksander</firstname>
+        <surname>Morgado</surname>
+        <affiliation>
+          <address>
+            <email>aleksander@aleksander.es</email>
+          </address>
+        </affiliation>
       </author>
     </authorgroup>
 
diff --git a/docs/reference/libmm-glib/libmm-glib-sections.txt b/docs/reference/libmm-glib/libmm-glib-sections.txt
index 583b494..690db8b 100644
--- a/docs/reference/libmm-glib/libmm-glib-sections.txt
+++ b/docs/reference/libmm-glib/libmm-glib-sections.txt
@@ -1012,6 +1012,18 @@
 mm_modem_voice_list_calls
 mm_modem_voice_list_calls_finish
 mm_modem_voice_list_calls_sync
+mm_modem_voice_hangup_and_accept
+mm_modem_voice_hangup_and_accept_finish
+mm_modem_voice_hangup_and_accept_sync
+mm_modem_voice_hold_and_accept
+mm_modem_voice_hold_and_accept_finish
+mm_modem_voice_hold_and_accept_sync
+mm_modem_voice_hangup_all
+mm_modem_voice_hangup_all_finish
+mm_modem_voice_hangup_all_sync
+mm_modem_voice_transfer
+mm_modem_voice_transfer_finish
+mm_modem_voice_transfer_sync
 <SUBSECTION Standard>
 MMModemVoiceClass
 MMModemVoicePrivate
@@ -1320,6 +1332,7 @@
 mm_call_dup_audio_port
 mm_call_get_audio_format
 mm_call_peek_audio_format
+mm_call_get_multiparty
 <SUBSECTION Methods>
 mm_call_start
 mm_call_start_finish
@@ -1333,6 +1346,15 @@
 mm_call_send_dtmf
 mm_call_send_dtmf_finish
 mm_call_send_dtmf_sync
+mm_call_deflect
+mm_call_deflect_finish
+mm_call_deflect_sync
+mm_call_join_multiparty
+mm_call_join_multiparty_finish
+mm_call_join_multiparty_sync
+mm_call_leave_multiparty
+mm_call_leave_multiparty_finish
+mm_call_leave_multiparty_sync
 <SUBSECTION Standard>
 MMCallClass
 MMCallPrivate
@@ -2835,6 +2857,18 @@
 mm_gdbus_modem_voice_call_list_calls
 mm_gdbus_modem_voice_call_list_calls_finish
 mm_gdbus_modem_voice_call_list_calls_sync
+mm_gdbus_modem_voice_call_hangup_and_accept
+mm_gdbus_modem_voice_call_hangup_and_accept_finish
+mm_gdbus_modem_voice_call_hangup_and_accept_sync
+mm_gdbus_modem_voice_call_hold_and_accept
+mm_gdbus_modem_voice_call_hold_and_accept_finish
+mm_gdbus_modem_voice_call_hold_and_accept_sync
+mm_gdbus_modem_voice_call_hangup_all
+mm_gdbus_modem_voice_call_hangup_all_finish
+mm_gdbus_modem_voice_call_hangup_all_sync
+mm_gdbus_modem_voice_call_transfer
+mm_gdbus_modem_voice_call_transfer_finish
+mm_gdbus_modem_voice_call_transfer_sync
 <SUBSECTION Private>
 mm_gdbus_modem_voice_set_calls
 mm_gdbus_modem_voice_emit_call_added
@@ -2842,6 +2876,10 @@
 mm_gdbus_modem_voice_complete_create_call
 mm_gdbus_modem_voice_complete_delete_call
 mm_gdbus_modem_voice_complete_list_calls
+mm_gdbus_modem_voice_complete_hangup_and_accept
+mm_gdbus_modem_voice_complete_hold_and_accept
+mm_gdbus_modem_voice_complete_hangup_all
+mm_gdbus_modem_voice_complete_transfer
 mm_gdbus_modem_voice_interface_info
 mm_gdbus_modem_voice_override_properties
 <SUBSECTION Standard>
@@ -3215,6 +3253,7 @@
 mm_gdbus_call_dup_audio_port
 mm_gdbus_call_get_audio_format
 mm_gdbus_call_get_audio_port
+mm_gdbus_call_get_multiparty
 <SUBSECTION Methods>
 mm_gdbus_call_call_accept
 mm_gdbus_call_call_accept_finish
@@ -3228,6 +3267,15 @@
 mm_gdbus_call_call_send_dtmf
 mm_gdbus_call_call_send_dtmf_finish
 mm_gdbus_call_call_send_dtmf_sync
+mm_gdbus_call_call_deflect
+mm_gdbus_call_call_deflect_finish
+mm_gdbus_call_call_deflect_sync
+mm_gdbus_call_call_join_multiparty
+mm_gdbus_call_call_join_multiparty_finish
+mm_gdbus_call_call_join_multiparty_sync
+mm_gdbus_call_call_leave_multiparty
+mm_gdbus_call_call_leave_multiparty_finish
+mm_gdbus_call_call_leave_multiparty_sync
 <SUBSECTION Private>
 mm_gdbus_call_set_direction
 mm_gdbus_call_set_number
@@ -3235,10 +3283,14 @@
 mm_gdbus_call_set_state_reason
 mm_gdbus_call_set_audio_format
 mm_gdbus_call_set_audio_port
+mm_gdbus_call_set_multiparty
 mm_gdbus_call_complete_accept
 mm_gdbus_call_complete_hangup
 mm_gdbus_call_complete_send_dtmf
 mm_gdbus_call_complete_start
+mm_gdbus_call_complete_deflect
+mm_gdbus_call_complete_join_multiparty
+mm_gdbus_call_complete_leave_multiparty
 mm_gdbus_call_interface_info
 mm_gdbus_call_override_properties
 mm_gdbus_call_emit_dtmf_received
diff --git a/include/ModemManager-compat.h b/include/ModemManager-compat.h
index 974b69b..6eb687e 100644
--- a/include/ModemManager-compat.h
+++ b/include/ModemManager-compat.h
@@ -27,6 +27,7 @@
 /**
  * SECTION:mm-compat
  * @title: API break replacements
+ * @short_description: List of deprecated methods, types and symbols.
  *
  * These compatibility types and methods are flagged as deprecated and
  * therefore shouldn't be used in newly written code. They are provided to
@@ -694,6 +695,20 @@
  */
 #define MM_MODEM_BAND_CDMA_BC19_US_LOWER_700 ((MMModemBandDeprecated)MM_MODEM_BAND_CDMA_BC19)
 
+/* The following type exists just so that we can get deprecation warnings */
+MM_DEPRECATED
+typedef int MMModemLocationSourceDeprecated;
+
+/**
+ * MM_MODEM_LOCATION_SOURCE_AGPS:
+ *
+ * A-GPS location requested.
+ *
+ * Since: 1.0
+ * Deprecated: 1.12.0: Use #MM_MODEM_LOCATION_SOURCE_AGPS_MSA instead.
+ */
+#define MM_MODEM_LOCATION_SOURCE_AGPS ((MMModemLocationSourceDeprecated)MM_MODEM_LOCATION_SOURCE_AGPS_MSA)
+
 #endif /* MM_DISABLE_DEPRECATED */
 
 #endif /* _MODEMMANAGER_COMPAT_H_ */
diff --git a/include/ModemManager-enums.h b/include/ModemManager-enums.h
index 9e75e2f..f84eafc 100644
--- a/include/ModemManager-enums.h
+++ b/include/ModemManager-enums.h
@@ -928,7 +928,8 @@
  * @MM_MODEM_LOCATION_SOURCE_GPS_NMEA: GPS location given as NMEA traces.
  * @MM_MODEM_LOCATION_SOURCE_CDMA_BS: CDMA base station position.
  * @MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED: No location given, just GPS module setup.
- * @MM_MODEM_LOCATION_SOURCE_AGPS: A-GPS location requested.
+ * @MM_MODEM_LOCATION_SOURCE_AGPS_MSA: Mobile Station Assisted A-GPS location requested. Since 1.12.
+ * @MM_MODEM_LOCATION_SOURCE_AGPS_MSB: Mobile Station Based A-GPS location requested. Since 1.12.
  *
  * Sources of location information supported by the modem.
  */
@@ -939,7 +940,13 @@
     MM_MODEM_LOCATION_SOURCE_GPS_NMEA      = 1 << 2,
     MM_MODEM_LOCATION_SOURCE_CDMA_BS       = 1 << 3,
     MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED = 1 << 4,
-    MM_MODEM_LOCATION_SOURCE_AGPS          = 1 << 5,
+    MM_MODEM_LOCATION_SOURCE_AGPS_MSA      = 1 << 5,
+    MM_MODEM_LOCATION_SOURCE_AGPS_MSB      = 1 << 6,
+#if defined (MM_COMPILATION)
+    /* MM internal methods, not part of the API */
+    MM_MODEM_LOCATION_SOURCE_FIRST = MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI, /*< skip >*/
+    MM_MODEM_LOCATION_SOURCE_LAST  = MM_MODEM_LOCATION_SOURCE_AGPS_MSB,    /*< skip >*/
+#endif
 } MMModemLocationSource;
 
 /**
@@ -1379,6 +1386,10 @@
  * @MM_CALL_STATE_REASON_REFUSED_OR_BUSY: Remote peer is busy or refused call.
  * @MM_CALL_STATE_REASON_ERROR: Wrong number or generic network error.
  * @MM_CALL_STATE_REASON_AUDIO_SETUP_FAILED: Error setting up audio channel.
+ * @MM_CALL_STATE_REASON_TRANSFERRED: Call has been transferred. Since 1.12.
+ * @MM_CALL_STATE_REASON_DEFLECTED: Call has been deflected to a new number. Since 1.12.
+ *
+ * Reason for the state change in the call.
  */
 typedef enum { /*< underscore_name=mm_call_state_reason >*/
     MM_CALL_STATE_REASON_UNKNOWN            = 0,
@@ -1388,7 +1399,9 @@
     MM_CALL_STATE_REASON_TERMINATED         = 4,
     MM_CALL_STATE_REASON_REFUSED_OR_BUSY    = 5,
     MM_CALL_STATE_REASON_ERROR              = 6,
-    MM_CALL_STATE_REASON_AUDIO_SETUP_FAILED = 7
+    MM_CALL_STATE_REASON_AUDIO_SETUP_FAILED = 7,
+    MM_CALL_STATE_REASON_TRANSFERRED        = 8,
+    MM_CALL_STATE_REASON_DEFLECTED          = 9,
 } MMCallStateReason;
 
 /**
@@ -1396,6 +1409,8 @@
  * @MM_CALL_DIRECTION_UNKNOWN: unknown.
  * @MM_CALL_DIRECTION_INCOMING: call from network.
  * @MM_CALL_DIRECTION_OUTGOING: call to network.
+ *
+ * Direction of the call.
  */
 typedef enum { /*< underscore_name=mm_call_direction >*/
     MM_CALL_DIRECTION_UNKNOWN   = 0,
diff --git a/include/ModemManager-tags.h b/include/ModemManager-tags.h
index 4be1e79..ed65af5 100644
--- a/include/ModemManager-tags.h
+++ b/include/ModemManager-tags.h
@@ -73,6 +73,20 @@
 #define ID_MM_DEVICE_PROCESS "ID_MM_DEVICE_PROCESS"
 
 /**
+ * ID_MM_DEVICE_IGNORE:
+ *
+ * This is a device-specific tag that allows explicitly requesting to
+ * ignore all ports exposed by the device.
+ *
+ * This tag was originally applicable to TTY ports and only when running
+ * in DEFAULT or PARANOID filter policy types. Since 1.12, this tag
+ * applies to all filter types (including STRICT), and to all port types
+ * (not only TTYs), and is associated to the
+ * MM_FILTER_RULE_EXPLICIT_BLACKLIST rule.
+ */
+#define ID_MM_DEVICE_IGNORE "ID_MM_DEVICE_IGNORE"
+
+/**
  * ID_MM_PORT_IGNORE:
  *
  * This is a port-specific tag that allows explicitly ignoring a given port
@@ -82,8 +96,8 @@
  */
 #define ID_MM_PORT_IGNORE "ID_MM_PORT_IGNORE"
 
-/**
- * ID_MM_DEVICE_IGNORE:
+ /**
+ * ID_MM_TTY_BLACKLIST:
  *
  * This is a device-specific tag that allows explicitly blacklisting
  * devices that expose TTY devices so that they are never probed.
@@ -94,10 +108,10 @@
  *
  * This tag is ignored when the STRICT filter policy is used.
  */
-#define ID_MM_DEVICE_IGNORE "ID_MM_DEVICE_IGNORE"
+#define ID_MM_TTY_BLACKLIST "ID_MM_TTY_BLACKLIST"
 
 /**
- * ID_MM_DEVICE_MANUAL_SCAN_ONLY:
+ * ID_MM_TTY_MANUAL_SCAN_ONLY:
  *
  * This is a device-specific tag that allows explicitly greylisting
  * devices that expose TTY devices so that they are never probed
@@ -111,23 +125,7 @@
  *
  * This tag is ignored when the STRICT filter policy is used.
  */
-#define ID_MM_DEVICE_MANUAL_SCAN_ONLY "ID_MM_DEVICE_MANUAL_SCAN_ONLY"
-
-/**
- * ID_MM_PLATFORM_DRIVER_PROBE:
- *
- * This is a port-specific tag applied to platform ports so that they
- * are probed automatically by the daemon. Platform ports that don't
- * have this tag will never probed. This tag is a bit redundant, as
- * the user could also use ID_MM_DEVICE_PROCESS for the same purpose.
- *
- * This tag is associated to the MM_FILTER_RULE_TTY_PLATFORM_DRIVER
- * rule, which is only meaningful when the daemon runs with the
- * DEFAULT filter policy type, as that is the only one that would
- * allow probing all ports not explicitly forbidden before the last
- * MM_FILTER_RULE_TTY_DEFAULT_ALLOWED rule.
- */
-#define ID_MM_PLATFORM_DRIVER_PROBE "ID_MM_PLATFORM_DRIVER_PROBE"
+#define ID_MM_TTY_MANUAL_SCAN_ONLY "ID_MM_TTY_MANUAL_SCAN_ONLY"
 
 /**
  * ID_MM_PORT_TYPE_AT_PRIMARY:
diff --git a/include/ModemManager-version.h.in b/include/ModemManager-version.h.in
index 0cb4341..695a1b0 100644
--- a/include/ModemManager-version.h.in
+++ b/include/ModemManager-version.h.in
@@ -58,6 +58,8 @@
  * @minor: minor version (e.g. 2 for version 1.2.5)
  * @micro: micro version (e.g. 5 for version 1.2.5)
  *
+ * Checks the version of ModemManager at compile time.
+ *
  * Returns: %TRUE if the version of the ModemManager header files
  * is the same as or newer than the passed-in version.
  */
diff --git a/introspection/org.freedesktop.ModemManager1.Call.xml b/introspection/org.freedesktop.ModemManager1.Call.xml
index f53f5b7b..659601b 100644
--- a/introspection/org.freedesktop.ModemManager1.Call.xml
+++ b/introspection/org.freedesktop.ModemManager1.Call.xml
@@ -5,6 +5,7 @@
 
    Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
    Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
+   Copyright (C) 2019 Purism SPC
 -->
 
 <node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
@@ -38,6 +39,48 @@
     <method name="Accept" />
 
     <!--
+        Deflect:
+        @number: new number where the call will be deflected.
+
+        Deflect an incoming or waiting call to a new number. This call will be
+        considered terminated once the deflection is performed.
+
+        Applicable only if state is <link linkend="MM-CALL-STATE-RINGING-IN:CAPS"><constant>MM_CALL_STATE_RINGING_IN</constant></link> or
+        <link linkend="MM-CALL-STATE-WAITING:CAPS"><constant>MM_CALL_STATE_WAITING</constant></link> and direction is
+        <link linkend="MM-CALL-DIRECTION-INCOMING:CAPS"><constant>MM_CALL_DIRECTION_INCOMING</constant></link>.
+    -->
+    <method name="Deflect">
+      <arg name="number" type="s" />
+    </method>
+
+    <!--
+        JoinMultiparty:
+
+        Join the currently held call into a single multiparty call with another
+        already active call.
+
+        The calls will be flagged with the
+        '<link linkend="gdbus-property-org-freedesktop-ModemManager1-Call.Multiparty">Multiparty</link>'
+        property while they are part of the multiparty call.
+
+        Applicable only if state is <link linkend="MM-CALL-STATE-HELD:CAPS"><constant>MM_CALL_STATE_HELD</constant></link>.
+    -->
+    <method name="JoinMultiparty" />
+
+    <!--
+        LeaveMultiparty:
+
+        If this call is part of an ongoing multiparty call, detach it from the multiparty call,
+        put the multiparty call on hold, and activate this one alone. This operation makes this
+        call private again between both ends of the call.
+
+        Applicable only if state is <link linkend="MM-CALL-STATE-ACTIVE:CAPS"><constant>MM_CALL_STATE_ACTIVE</constant></link> or
+        <link linkend="MM-CALL-STATE-HELD:CAPS"><constant>MM_CALL_STATE_HELD</constant></link> and
+        the call is a multiparty call.
+    -->
+    <method name="LeaveMultiparty"/>
+
+    <!--
         Hangup:
 
         Hangup the active call.
@@ -113,6 +156,13 @@
     <property name="Number" type="s" access="read" />
 
     <!--
+        Multiparty:
+
+        Whether the call is currently part of a multiparty conference call.
+    -->
+    <property name="Multiparty" type="b" access="read" />
+
+    <!--
         AudioPort:
 
         If call audio is routed via the host, the name of the kernel device that
diff --git a/introspection/org.freedesktop.ModemManager1.Modem.Location.xml b/introspection/org.freedesktop.ModemManager1.Modem.Location.xml
index 5c49abb..d785a24 100644
--- a/introspection/org.freedesktop.ModemManager1.Modem.Location.xml
+++ b/introspection/org.freedesktop.ModemManager1.Modem.Location.xml
@@ -47,12 +47,20 @@
         location information.
 
         The optional
-        <link linkend="MM-MODEM-LOCATION-SOURCE-AGPS:CAPS">MM_MODEM_LOCATION_SOURCE_AGPS</link>
-        allows to request A-GPS operation, and it must be given along with either
+        <link linkend="MM-MODEM-LOCATION-SOURCE-AGPS-MSA:CAPS">MM_MODEM_LOCATION_SOURCE_AGPS_MSA</link>
+        and
+        <link linkend="MM-MODEM-LOCATION-SOURCE-AGPS-MSB:CAPS">MM_MODEM_LOCATION_SOURCE_AGPS_MSB</link>
+        allow to request MSA/MSB A-GPS operation, and they must be given along with either
         <link linkend="MM-MODEM-LOCATION-SOURCE-GPS-RAW:CAPS">MM_MODEM_LOCATION_SOURCE_GPS_RAW</link>
         or
         <link linkend="MM-MODEM-LOCATION-SOURCE-GPS-NMEA:CAPS">MM_MODEM_LOCATION_SOURCE_GPS_NMEA</link>.
 
+        Both
+        <link linkend="MM-MODEM-LOCATION-SOURCE-AGPS-MSA:CAPS">MM_MODEM_LOCATION_SOURCE_AGPS_MSA</link>
+        and
+        <link linkend="MM-MODEM-LOCATION-SOURCE-AGPS-MSB:CAPS">MM_MODEM_LOCATION_SOURCE_AGPS_MSB</link>
+        cannot be given at the same time, and if none given, standalone GPS is assumed.
+
     -->
     <method name="Setup">
       <arg name="sources"         type="u" direction="in" />
diff --git a/introspection/org.freedesktop.ModemManager1.Modem.Voice.xml b/introspection/org.freedesktop.ModemManager1.Modem.Voice.xml
index 4912ba5..59f11e9 100644
--- a/introspection/org.freedesktop.ModemManager1.Modem.Voice.xml
+++ b/introspection/org.freedesktop.ModemManager1.Modem.Voice.xml
@@ -5,6 +5,7 @@
 
    Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
    Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
+   Copyright (C) 2019 Purism SPC
 -->
 
 <node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
@@ -64,10 +65,95 @@
     </method>
 
     <!--
+        HoldAndAccept:
+
+        Place all active calls on hold, if any, and accept the next
+        call.
+
+        Waiting calls have preference over held calls, so the next
+        call being active will be any waiting call, or otherwise,
+        any held call.
+
+        The user should monitor the state of all available ongoing
+        calls to be reported of which one becomes active.
+
+        No error is returned if there are no waiting or held calls.
+    -->
+    <method name="HoldAndAccept" />
+
+    <!--
+        HangupAndAccept:
+
+        Hangup all active calls, if any, and accept the next call.
+
+        Waiting calls have preference over held calls, so the next
+        call being active will be any waiting call, or otherwise,
+        any held call.
+
+        The user should monitor the state of all available ongoing
+        calls to be reported of which one becomes active.
+
+        No error is returned if there are no waiting or held calls.
+        In this case, this method would be equivalent to calling
+        <link linkend="gdbus-method-org-freedesktop-ModemManager1-Call.Hangup">Hangup()</link>
+        on the active call.
+    -->
+    <method name="HangupAndAccept" />
+
+    <!--
+        HangupAll:
+
+        Hangup all active calls.
+
+        Depending on how the device implements the action, calls on
+        hold or in waiting state may also be terminated.
+
+        No error is returned if there are no ongoing calls.
+    -->
+    <method name="HangupAll" />
+
+    <!--
+        Transfer:
+
+        Join the currently active and held calls together into a single
+        multiparty call, but disconnects from them.
+
+        The affected calls will be considered terminated from the point of
+        view of the subscriber.
+    -->
+    <method name="Transfer" />
+
+    <!--
+        CallWaitingSetup:
+
+        Activates or deactivates the call waiting network service, as per
+        3GPP TS 22.083.
+
+        This operation requires communication with the network in order to
+        complete, so the modem must be successfully registered.
+    -->
+    <method name="CallWaitingSetup">
+      <arg name="enable" type="b" direction="in" />
+    </method>
+
+    <!--
+        CallWaitingQuery:
+
+        Queries the status of the call waiting network service, as per
+        3GPP TS 22.083.
+
+        This operation requires communication with the network in order to
+        complete, so the modem must be successfully registered.
+    -->
+    <method name="CallWaitingQuery">
+      <arg name="status" type="b" direction="out" />
+    </method>
+
+    <!--
         CallAdded:
         @path: Object path of the new call.
 
-        Emitted when any part of a Call has been received or added.
+        Emitted when a call has been added.
     -->
     <signal name="CallAdded">
       <arg name="path" type="o" />
diff --git a/introspection/org.freedesktop.ModemManager1.Modem.xml b/introspection/org.freedesktop.ModemManager1.Modem.xml
index 0fc5741..eb4debd 100644
--- a/introspection/org.freedesktop.ModemManager1.Modem.xml
+++ b/introspection/org.freedesktop.ModemManager1.Modem.xml
@@ -132,11 +132,11 @@
 
     <!--
         SetPowerState:
-	@state: A <link linkend="MMModemPowerState">MMModemPowerState</link> value, to specify the desired power state.
+        @state: A <link linkend="MMModemPowerState">MMModemPowerState</link> value, to specify the desired power state.
 
         Set the power state of the modem. This action can only be run when the
-	modem is in <link linkend="MM-MODEM-STATE-DISABLED:CAPS"><constant>MM_MODEM_STATE_DISABLED</constant></link>
-	state.
+        modem is in <link linkend="MM-MODEM-STATE-DISABLED:CAPS"><constant>MM_MODEM_STATE_DISABLED</constant></link>
+        state.
     -->
     <method name="SetPowerState">
       <arg name="state" type="u" direction="in" />
@@ -424,9 +424,9 @@
         StateFailedReason:
 
         Error specifying why the modem is in
-	<link linkend="MM-MODEM-STATE-FAILED:CAPS"><constant>MM_MODEM_STATE_FAILED</constant></link>
-	state, given as a
-	<link linkend="MMModemStateFailedReason">MMModemStateFailedReason</link> value.
+        <link linkend="MM-MODEM-STATE-FAILED:CAPS"><constant>MM_MODEM_STATE_FAILED</constant></link>
+        state, given as a
+        <link linkend="MMModemStateFailedReason">MMModemStateFailedReason</link> value.
     -->
     <property name="StateFailedReason" type="u" access="read" />
 
@@ -466,8 +466,8 @@
     <!--
         PowerState:
 
-	A <link linkend="MMModemPowerState">MMModemPowerState</link> value
-	specifying the current power state of the modem.
+        A <link linkend="MMModemPowerState">MMModemPowerState</link> value
+        specifying the current power state of the modem.
     -->
     <property name="PowerState" type="u" access="read" />
 
diff --git a/introspection/org.freedesktop.ModemManager1.Sms.xml b/introspection/org.freedesktop.ModemManager1.Sms.xml
index 1bd2150..91df019 100644
--- a/introspection/org.freedesktop.ModemManager1.Sms.xml
+++ b/introspection/org.freedesktop.ModemManager1.Sms.xml
@@ -109,8 +109,8 @@
          <varlistentry><term><link linkend="MM-SMS-VALIDITY-TYPE-RELATIVE:CAPS">MM_SMS_VALIDITY_TYPE_RELATIVE</link></term>
           <listitem>
            <para>
-	        The value is the length of the validity period in minutes, given
-	        as an unsigned integer (D-Bus signature <literal>'u'</literal>).
+             The value is the length of the validity period in minutes, given
+             as an unsigned integer (D-Bus signature <literal>'u'</literal>).
             </para>
           </listitem>
          </varlistentry>
diff --git a/libmm-glib/mm-call.c b/libmm-glib/mm-call.c
index af1d8f1..91439f5 100644
--- a/libmm-glib/mm-call.c
+++ b/libmm-glib/mm-call.c
@@ -148,6 +148,24 @@
 /*****************************************************************************/
 
 /**
+ * mm_call_get_multiparty:
+ * @self: A #MMCall.
+ *
+ * Gets whether the call is part of a multiparty call.
+ *
+ * Returns: %TRUE if the call is part of a multiparty call, %FALSE otherwise..
+ */
+gboolean
+mm_call_get_multiparty (MMCall *self)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_get_multiparty (MM_GDBUS_CALL (self));
+}
+
+/*****************************************************************************/
+
+/**
  * mm_call_get_state:
  * @self: A #MMCall.
  *
@@ -492,6 +510,241 @@
 /*****************************************************************************/
 
 /**
+ * mm_call_deflect_finish:
+ * @self: A #MMCall.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_call_deflect().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_call_deflect().
+ *
+ * Returns:  %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_deflect_finish (MMCall *self,
+                        GAsyncResult *res,
+                        GError **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_deflect_finish (MM_GDBUS_CALL (self), res, error);
+}
+
+/**
+ * mm_call_deflect:
+ * @self: A #MMCall.
+ * @number: new number where the call will be deflected.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously requests to deflect the incoming call.
+ *
+ * This call will be considered terminated once the deflection is performed.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_call_deflect_finish() to get the result of the operation.
+ *
+ * See mm_call_deflect_sync() for the synchronous, blocking version of this method.
+ */
+void
+mm_call_deflect (MMCall *self,
+                 const gchar *number,
+                 GCancellable *cancellable,
+                 GAsyncReadyCallback callback,
+                 gpointer user_data)
+{
+    g_return_if_fail (MM_IS_CALL (self));
+
+    mm_gdbus_call_call_deflect (MM_GDBUS_CALL (self),
+                                number,
+                                cancellable,
+                                callback,
+                                user_data);
+}
+
+/**
+ * mm_call_deflect_sync:
+ * @self: A #MMCall.
+ * @number: new number where the call will be deflected.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously requests to deflect the incoming call.
+ *
+ * This call will be considered terminated once the deflection is performed.
+ *
+ * The calling thread is blocked until an incoming call is ready.
+ * See mm_call_deflect() for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_deflect_sync (MMCall *self,
+                      const gchar *number,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_deflect_sync (MM_GDBUS_CALL (self),
+                                            number,
+                                            cancellable,
+                                            error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_call_join_multiparty_finish:
+ * @self: A #MMCall.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_call_join_multiparty().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_call_join_multiparty().
+ *
+ * Returns:  %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_join_multiparty_finish (MMCall        *self,
+                                GAsyncResult  *res,
+                                GError       **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_join_multiparty_finish (MM_GDBUS_CALL (self), res, error);
+}
+
+/**
+ * mm_call_join_multiparty:
+ * @self: A #MMCall.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Synchronously requests to join this call into a multiparty call.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_call_join_multiparty_finish() to get the result of the operation.
+ *
+ * See mm_call_join_multiparty_sync() for the synchronous, blocking version of this method.
+ */
+void
+mm_call_join_multiparty (MMCall              *self,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_CALL (self));
+
+    mm_gdbus_call_call_join_multiparty (MM_GDBUS_CALL (self),
+                                        cancellable,
+                                        callback,
+                                        user_data);
+}
+
+/**
+ * mm_call_join_multiparty_sync:
+ * @self: A #MMCall.g
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously requests to join this call into a multiparty call.
+ *
+ * The calling thread is blocked until an incoming call is ready.
+ * See mm_call_join_multiparty() for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_join_multiparty_sync (MMCall       *self,
+                              GCancellable *cancellable,
+                              GError       **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_join_multiparty_sync (MM_GDBUS_CALL (self),
+                                                    cancellable,
+                                                    error);
+}
+
+/**
+ * mm_call_leave_multiparty_finish:
+ * @self: A #MMCall.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_call_leave_multiparty().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_call_leave_multiparty().
+ *
+ * Returns:  %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_leave_multiparty_finish (MMCall        *self,
+                                 GAsyncResult  *res,
+                                 GError       **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_leave_multiparty_finish (MM_GDBUS_CALL (self), res, error);
+}
+
+/**
+ * mm_call_leave_multiparty:
+ * @self: A #MMCall.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Synchronously requests to make this call private again by leaving the
+ * multiparty call.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_call_leave_multiparty_finish() to get the result of the operation.
+ *
+ * See mm_call_leave_multiparty_sync() for the synchronous, blocking version of this method.
+ */
+void
+mm_call_leave_multiparty (MMCall              *self,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_CALL (self));
+
+    mm_gdbus_call_call_leave_multiparty (MM_GDBUS_CALL (self),
+                                         cancellable,
+                                         callback,
+                                         user_data);
+}
+
+/**
+ * mm_call_leave_multiparty_sync:
+ * @self: A #MMCall.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously requests to make this call private again by leaving the
+ * multiparty call.
+ *
+ * The calling thread is blocked until an incoming call is ready.
+ * See mm_call_leave_multiparty() for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation succeeded, %FALSE if @error is set.
+ */
+gboolean
+mm_call_leave_multiparty_sync (MMCall       *self,
+                               GCancellable *cancellable,
+                               GError       **error)
+{
+    g_return_val_if_fail (MM_IS_CALL (self), FALSE);
+
+    return mm_gdbus_call_call_leave_multiparty_sync (MM_GDBUS_CALL (self),
+                                                     cancellable,
+                                                     error);
+}
+
+ /*****************************************************************************/
+
+/**
  * mm_call_hangup_finish:
  * @self: A #MMCall.
  * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_call_hangup().
diff --git a/libmm-glib/mm-call.h b/libmm-glib/mm-call.h
index e6d8957..3ae5a77 100644
--- a/libmm-glib/mm-call.h
+++ b/libmm-glib/mm-call.h
@@ -76,6 +76,8 @@
 
 MMCallDirection    mm_call_get_direction    (MMCall *self);
 
+gboolean           mm_call_get_multiparty   (MMCall *self);
+
 const gchar       *mm_call_get_audio_port   (MMCall *self);
 gchar             *mm_call_dup_audio_port   (MMCall *self);
 
@@ -110,6 +112,46 @@
                                              GCancellable *cancellable,
                                              GError **error);
 
+void               mm_call_deflect          (MMCall *self,
+                                             const gchar *number,
+                                             GCancellable *cancellable,
+                                             GAsyncReadyCallback callback,
+                                             gpointer user_data);
+
+gboolean           mm_call_deflect_finish   (MMCall *self,
+                                             GAsyncResult *res,
+                                             GError **error);
+
+gboolean           mm_call_deflect_sync     (MMCall *self,
+                                             const gchar *number,
+                                             GCancellable *cancellable,
+                                             GError **error);
+
+void               mm_call_join_multiparty        (MMCall              *self,
+                                                   GCancellable        *cancellable,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data);
+
+gboolean           mm_call_join_multiparty_finish (MMCall        *self,
+                                                   GAsyncResult  *res,
+                                                   GError       **error);
+
+gboolean           mm_call_join_multiparty_sync   (MMCall        *self,
+                                                   GCancellable  *cancellable,
+                                                   GError       **error);
+
+void               mm_call_leave_multiparty        (MMCall              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+
+gboolean           mm_call_leave_multiparty_finish (MMCall        *self,
+                                                    GAsyncResult  *res,
+                                                    GError       **error);
+
+gboolean           mm_call_leave_multiparty_sync   (MMCall        *self,
+                                                    GCancellable  *cancellable,
+                                                    GError       **error);
 
 void               mm_call_hangup           (MMCall *self,
                                              GCancellable *cancellable,
diff --git a/libmm-glib/mm-common-helpers.c b/libmm-glib/mm-common-helpers.c
index a3cd16a..8673421 100644
--- a/libmm-glib/mm-common-helpers.c
+++ b/libmm-glib/mm-common-helpers.c
@@ -722,6 +722,20 @@
     return !different;
 }
 
+gboolean
+mm_common_bands_garray_lookup (GArray      *array,
+                               MMModemBand  value)
+{
+    guint i;
+
+    for (i = 0; i < array->len; i++) {
+        if (value == g_array_index (array, MMModemBand, i))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
 void
 mm_common_bands_garray_sort (GArray *array)
 {
diff --git a/libmm-glib/mm-common-helpers.h b/libmm-glib/mm-common-helpers.h
index 71ef978..5af5842 100644
--- a/libmm-glib/mm-common-helpers.h
+++ b/libmm-glib/mm-common-helpers.h
@@ -103,8 +103,9 @@
 GVariant    *mm_common_build_bands_any     (void);
 GVariant    *mm_common_build_bands_unknown (void);
 
-gboolean     mm_common_bands_garray_cmp  (GArray *a, GArray *b);
-void         mm_common_bands_garray_sort (GArray *array);
+gboolean     mm_common_bands_garray_cmp    (GArray *a, GArray *b);
+void         mm_common_bands_garray_sort   (GArray *array);
+gboolean     mm_common_bands_garray_lookup (GArray *array, MMModemBand value);
 
 gboolean mm_common_band_is_gsm    (MMModemBand band);
 gboolean mm_common_band_is_utran  (MMModemBand band);
diff --git a/libmm-glib/mm-firmware-update-settings.c b/libmm-glib/mm-firmware-update-settings.c
index 37c8381..fc8a45b 100644
--- a/libmm-glib/mm-firmware-update-settings.c
+++ b/libmm-glib/mm-firmware-update-settings.c
@@ -174,7 +174,7 @@
     g_variant_builder_add (&builder, "u", method);
 
     g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}"));
-    {
+    if (self) {
         g_variant_builder_add (&builder,
                                "{sv}",
                                PROPERTY_DEVICE_IDS,
diff --git a/libmm-glib/mm-modem-voice.c b/libmm-glib/mm-modem-voice.c
index db70355..e23a730 100644
--- a/libmm-glib/mm-modem-voice.c
+++ b/libmm-glib/mm-modem-voice.c
@@ -532,6 +532,491 @@
 
 /*****************************************************************************/
 
+/**
+ * mm_modem_voice_hold_and_accept_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_hold_and_accept().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_hold_and_accept().
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hold_and_accept_finish (MMModemVoice  *self,
+                                       GAsyncResult  *res,
+                                       GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hold_and_accept_finish (MM_GDBUS_MODEM_VOICE (self), res, error);
+}
+
+/**
+ * mm_modem_voice_hold_and_accept:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously puts all active calls on hold and accepts the next waiting or held call.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_hold_and_accept_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_hold_and_accept_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_hold_and_accept (MMModemVoice        *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_hold_and_accept (MM_GDBUS_MODEM_VOICE (self),
+                                               cancellable,
+                                               callback,
+                                               user_data);
+}
+
+/**
+ * mm_modem_voice_hold_and_accept_sync:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously puts all active calls on hold and accepts the next waiting or held call.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_hold_and_accept()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hold_and_accept_sync (MMModemVoice  *self,
+                                     GCancellable  *cancellable,
+                                     GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hold_and_accept_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                           cancellable,
+                                                           error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_modem_voice_hangup_and_accept_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_hangup_and_accept().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_hangup_and_accept().
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hangup_and_accept_finish (MMModemVoice  *self,
+                                         GAsyncResult  *res,
+                                         GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hangup_and_accept_finish (MM_GDBUS_MODEM_VOICE (self), res, error);
+}
+
+/**
+ * mm_modem_voice_hangup_and_accept:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously hangs up all active calls and accepts the next waiting or held call.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_hangup_and_accept_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_hangup_and_accept_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_hangup_and_accept (MMModemVoice        *self,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_hangup_and_accept (MM_GDBUS_MODEM_VOICE (self),
+                                                 cancellable,
+                                                 callback,
+                                                 user_data);
+}
+
+/**
+ * mm_modem_voice_hangup_and_accept_sync:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously hangs up all active calls and accepts the next waiting or held call.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_hangup_and_accept()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hangup_and_accept_sync (MMModemVoice  *self,
+                                       GCancellable  *cancellable,
+                                       GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hangup_and_accept_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                             cancellable,
+                                                             error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_modem_voice_hangup_all_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_hangup_all().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_hangup_all().
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hangup_all_finish (MMModemVoice  *self,
+                                  GAsyncResult  *res,
+                                  GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hangup_all_finish (MM_GDBUS_MODEM_VOICE (self), res, error);
+}
+
+/**
+ * mm_modem_voice_hangup_all:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously hangs up all ongoing (active, waiting, held) calls.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_hangup_all_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_hangup_all_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_hangup_all (MMModemVoice        *self,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_hangup_all (MM_GDBUS_MODEM_VOICE (self),
+                                          cancellable,
+                                          callback,
+                                          user_data);
+}
+
+/**
+ * mm_modem_voice_hangup_all_sync:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously hangs up all ongoing (active, waiting, held) calls.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_hangup_all()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_hangup_all_sync (MMModemVoice  *self,
+                                GCancellable  *cancellable,
+                                GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_hangup_all_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                      cancellable,
+                                                      error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_modem_voice_transfer_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_transfer().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_transfer().
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_transfer_finish (MMModemVoice  *self,
+                                GAsyncResult  *res,
+                                GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_transfer_finish (MM_GDBUS_MODEM_VOICE (self), res, error);
+}
+
+/**
+ * mm_modem_voice_transfer:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously joins all active and held calls, and disconnects from them.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_transfer_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_transfer_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_transfer (MMModemVoice        *self,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_transfer (MM_GDBUS_MODEM_VOICE (self),
+                                        cancellable,
+                                        callback,
+                                        user_data);
+}
+
+/**
+ * mm_modem_voice_transfer_sync:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously joins all active and held calls, and disconnects from them.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_transfer()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation was successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_transfer_sync (MMModemVoice  *self,
+                              GCancellable  *cancellable,
+                              GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_transfer_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                    cancellable,
+                                                    error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_modem_voice_call_waiting_setup_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_call_waiting_setup().
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_call_waiting_setup().
+ *
+ * Returns: %TRUE if  @status is set, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_call_waiting_setup_finish (MMModemVoice  *self,
+                                          GAsyncResult  *res,
+                                          GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_call_waiting_setup_finish (MM_GDBUS_MODEM_VOICE (self), res, error);
+}
+
+/**
+ * mm_modem_voice_call_waiting_setup:
+ * @self: A #MMModemVoice.
+ * @enable: Whether the call waiting service should be enabled.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously enables or disables the call waiting network service.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_call_waiting_setup_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_call_waiting_setup_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_call_waiting_setup (MMModemVoice        *self,
+                                   gboolean             enable,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_call_waiting_setup (MM_GDBUS_MODEM_VOICE (self),
+                                                  enable,
+                                                  cancellable,
+                                                  callback,
+                                                  user_data);
+}
+
+/**
+ * mm_modem_voice_call_waiting_setup_sync:
+ * @self: A #MMModemVoice.
+ * @enable: Whether the call waiting service should be enabled.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously enables or disables the call waiting network service.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_call_waiting_setup()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if the operation is successful, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_call_waiting_setup_sync (MMModemVoice  *self,
+                                        gboolean       enable,
+                                        GCancellable  *cancellable,
+                                        GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_call_waiting_setup_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                              enable,
+                                                              cancellable,
+                                                              error);
+}
+
+/*****************************************************************************/
+
+/**
+ * mm_modem_voice_call_waiting_query_finish:
+ * @self: A #MMModemVoice.
+ * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to mm_modem_voice_call_waiting_query().
+ * @status: Output location where to store the status.
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with mm_modem_voice_call_waiting_query().
+ *
+ * Returns: %TRUE if @status is set, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_call_waiting_query_finish (MMModemVoice  *self,
+                                          GAsyncResult  *res,
+                                          gboolean      *status,
+                                          GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_call_waiting_query_finish (MM_GDBUS_MODEM_VOICE (self), status, res, error);
+}
+
+/**
+ * mm_modem_voice_call_waiting_query:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
+ * @user_data: User data to pass to @callback.
+ *
+ * Asynchronously queries the status of the call waiting network service.
+ *
+ * When the operation is finished, @callback will be invoked in the <link linkend="g-main-context-push-thread-default">thread-default main loop</link> of the thread you are calling this method from.
+ * You can then call mm_modem_voice_call_waiting_query_finish() to get the result of the operation.
+ *
+ * See mm_modem_voice_call_waiting_query_sync() for the synchronous, blocking version of this method.
+ *
+ * Since: 1.12
+ */
+void
+mm_modem_voice_call_waiting_query (MMModemVoice        *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+    g_return_if_fail (MM_IS_MODEM_VOICE (self));
+
+    mm_gdbus_modem_voice_call_call_waiting_query (MM_GDBUS_MODEM_VOICE (self),
+                                                  cancellable,
+                                                  callback,
+                                                  user_data);
+}
+
+/**
+ * mm_modem_voice_call_waiting_query_sync:
+ * @self: A #MMModemVoice.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @status: Output location where to store the status.
+ * @error: Return location for error or %NULL.
+ *
+ * Synchronously queries the status of the call waiting network service.
+ *
+ * The calling thread is blocked until a reply is received. See mm_modem_voice_call_waiting_query()
+ * for the asynchronous version of this method.
+ *
+ * Returns: %TRUE if @status is set, %FALSE if @error is set.
+ * Since: 1.12
+ */
+gboolean
+mm_modem_voice_call_waiting_query_sync (MMModemVoice  *self,
+                                        GCancellable  *cancellable,
+                                        gboolean      *status,
+                                        GError       **error)
+{
+    g_return_val_if_fail (MM_IS_MODEM_VOICE (self), FALSE);
+
+    return mm_gdbus_modem_voice_call_call_waiting_query_sync (MM_GDBUS_MODEM_VOICE (self),
+                                                              status,
+                                                              cancellable,
+                                                              error);
+}
+
+/*****************************************************************************/
+
 static void
 mm_modem_voice_init (MMModemVoice *self)
 {
diff --git a/libmm-glib/mm-modem-voice.h b/libmm-glib/mm-modem-voice.h
index 2f33447..f43a60a 100644
--- a/libmm-glib/mm-modem-voice.h
+++ b/libmm-glib/mm-modem-voice.h
@@ -110,6 +110,76 @@
                                              GCancellable *cancellable,
                                              GError **error);
 
+void     mm_modem_voice_hold_and_accept         (MMModemVoice        *self,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+gboolean mm_modem_voice_hold_and_accept_finish  (MMModemVoice        *self,
+                                                 GAsyncResult        *res,
+                                                 GError             **error);
+gboolean mm_modem_voice_hold_and_accept_sync    (MMModemVoice        *self,
+                                                 GCancellable        *cancellable,
+                                                 GError             **error);
+
+void     mm_modem_voice_hangup_and_accept         (MMModemVoice        *self,
+                                                   GCancellable        *cancellable,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data);
+gboolean mm_modem_voice_hangup_and_accept_finish  (MMModemVoice        *self,
+                                                   GAsyncResult        *res,
+                                                   GError             **error);
+gboolean mm_modem_voice_hangup_and_accept_sync    (MMModemVoice        *self,
+                                                   GCancellable        *cancellable,
+                                                   GError             **error);
+
+void     mm_modem_voice_hangup_all         (MMModemVoice        *self,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean mm_modem_voice_hangup_all_finish  (MMModemVoice        *self,
+                                            GAsyncResult        *res,
+                                            GError             **error);
+gboolean mm_modem_voice_hangup_all_sync    (MMModemVoice        *self,
+                                            GCancellable        *cancellable,
+                                            GError             **error);
+
+void     mm_modem_voice_transfer         (MMModemVoice        *self,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data);
+gboolean mm_modem_voice_transfer_finish  (MMModemVoice        *self,
+                                          GAsyncResult        *res,
+                                          GError             **error);
+gboolean mm_modem_voice_transfer_sync    (MMModemVoice        *self,
+                                          GCancellable        *cancellable,
+                                          GError             **error);
+
+void     mm_modem_voice_call_waiting_setup         (MMModemVoice        *self,
+                                                    gboolean             enable,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean mm_modem_voice_call_waiting_setup_finish  (MMModemVoice        *self,
+                                                    GAsyncResult        *res,
+                                                    GError             **error);
+gboolean mm_modem_voice_call_waiting_setup_sync    (MMModemVoice        *self,
+                                                    gboolean             enable,
+                                                    GCancellable        *cancellable,
+                                                    GError             **error);
+
+void     mm_modem_voice_call_waiting_query         (MMModemVoice        *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean mm_modem_voice_call_waiting_query_finish  (MMModemVoice        *self,
+                                                    GAsyncResult        *res,
+                                                    gboolean            *status,
+                                                    GError             **error);
+gboolean mm_modem_voice_call_waiting_query_sync    (MMModemVoice        *self,
+                                                    GCancellable        *cancellable,
+                                                    gboolean            *status,
+                                                    GError             **error);
+
 G_END_DECLS
 
 #endif /* _MM_MODEM_VOICE_H_ */
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 016ef55..dd66d2f 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -78,6 +78,8 @@
 	tests/test-fixture.c \
 	tests/test-port-context.h \
 	tests/test-port-context.c \
+	tests/test-helpers.h \
+	tests/test-helpers.c \
 	$(NULL)
 libmm_test_common_la_CPPFLAGS = \
 	-I$(top_builddir)/libmm-glib/generated/tests \
@@ -98,11 +100,12 @@
 	-I$(top_srcdir)/libmm-glib/generated \
 	-I$(top_builddir)/libmm-glib/generated \
 	-I$(top_builddir)/libmm-glib/generated/tests \
-	-DCOMMON_GSM_PORT_CONF=\""$(abs_top_srcdir)/plugins/tests/gsm-port.conf"\"
+	-DCOMMON_GSM_PORT_CONF=\""$(abs_top_srcdir)/plugins/tests/gsm-port.conf"\" \
+	$(NULL)
 
 TEST_COMMON_LIBADD_FLAGS = \
 	$(builddir)/libmm-test-common.la \
-	$(top_builddir)/libmm-glib/libmm-glib.la
+	$(NULL)
 
 ################################################################################
 # common icera support
@@ -310,7 +313,10 @@
 noinst_PROGRAMS += test-service-generic
 test_service_generic_SOURCES  = generic/tests/test-service-generic.c
 test_service_generic_CPPFLAGS = $(TEST_COMMON_COMPILER_FLAGS)
-test_service_generic_LDADD    = $(TEST_COMMON_LIBADD_FLAGS)
+test_service_generic_LDADD    = \
+	$(top_builddir)/libmm-glib/libmm-glib.la \
+	$(TEST_COMMON_LIBADD_FLAGS) \
+	$(NULL)
 
 ################################################################################
 # plugin: motorola
@@ -355,8 +361,6 @@
 	huawei/mm-plugin-huawei.h \
 	huawei/mm-sim-huawei.c \
 	huawei/mm-sim-huawei.h \
-	huawei/mm-call-huawei.c \
-	huawei/mm-call-huawei.h \
 	huawei/mm-broadband-modem-huawei.c \
 	huawei/mm-broadband-modem-huawei.h \
 	huawei/mm-broadband-bearer-huawei.c \
@@ -897,8 +901,12 @@
 test_modem_helpers_telit_SOURCES = \
 	telit/tests/test-mm-modem-helpers-telit.c \
 	$(NULL)
-test_modem_helpers_telit_CPPFLAGS = $(PLUGIN_TELIT_COMPILER_FLAGS)
+test_modem_helpers_telit_CPPFLAGS = \
+	$(PLUGIN_TELIT_COMPILER_FLAGS) \
+	$(TEST_COMMON_COMPILER_FLAGS) \
+	$(NULL)
 test_modem_helpers_telit_LDADD = \
+	$(TEST_COMMON_LIBADD_FLAGS) \
 	$(builddir)/libhelpers-telit.la \
 	$(top_builddir)/src/libhelpers.la \
 	$(top_builddir)/libmm-glib/libmm-glib.la \
@@ -1018,8 +1026,12 @@
 test_modem_helpers_ublox_SOURCES = \
 	ublox/tests/test-modem-helpers-ublox.c \
 	$(NULL)
-test_modem_helpers_ublox_CPPFLAGS = $(PLUGIN_UBLOX_COMPILER_FLAGS)
+test_modem_helpers_ublox_CPPFLAGS = \
+	$(PLUGIN_UBLOX_COMPILER_FLAGS) \
+	$(TEST_COMMON_COMPILER_FLAGS) \
+	$(NULL)
 test_modem_helpers_ublox_LDADD = \
+	$(TEST_COMMON_LIBADD_FLAGS) \
 	$(builddir)/libhelpers-ublox.la \
 	$(top_builddir)/src/libhelpers.la \
 	$(top_builddir)/libmm-glib/libmm-glib.la \
@@ -1029,8 +1041,6 @@
 libmm_plugin_ublox_la_SOURCES = \
 	ublox/mm-plugin-ublox.c \
 	ublox/mm-plugin-ublox.h \
-	ublox/mm-call-ublox.c \
-	ublox/mm-call-ublox.h \
 	ublox/mm-broadband-bearer-ublox.h \
 	ublox/mm-broadband-bearer-ublox.c \
 	ublox/mm-broadband-modem-ublox.h \
@@ -1089,6 +1099,8 @@
 # plugin: quectel
 ################################################################################
 
+dist_udevrules_DATA += quectel/77-mm-quectel-port-types.rules
+
 pkglib_LTLIBRARIES += libmm-plugin-quectel.la
 libmm_plugin_quectel_la_SOURCES = \
 	quectel/mm-plugin-quectel.c \
@@ -1125,6 +1137,38 @@
 AM_CFLAGS += -DTESTUDEVRULESDIR_FIBOCOM=\"${srcdir}/fibocom\"
 
 ################################################################################
+# plugin: dlink
+################################################################################
+
+pkglib_LTLIBRARIES += libmm-plugin-dlink.la
+libmm_plugin_dlink_la_SOURCES = \
+	dlink/mm-plugin-dlink.c \
+	dlink/mm-plugin-dlink.h \
+	$(NULL)
+libmm_plugin_dlink_la_CPPFLAGS = $(PLUGIN_COMMON_COMPILER_FLAGS)
+libmm_plugin_dlink_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
+
+dist_udevrules_DATA += dlink/77-mm-dlink-port-types.rules
+
+AM_CFLAGS += -DTESTUDEVRULESDIR_DLINK=\"${srcdir}/dlink\"
+
+################################################################################
+# plugin: tplink
+################################################################################
+
+pkglib_LTLIBRARIES += libmm-plugin-tplink.la
+libmm_plugin_tplink_la_SOURCES = \
+	tplink/mm-plugin-tplink.c \
+	tplink/mm-plugin-tplink.h \
+	$(NULL)
+libmm_plugin_tplink_la_CPPFLAGS = $(PLUGIN_COMMON_COMPILER_FLAGS)
+libmm_plugin_tplink_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
+
+dist_udevrules_DATA += tplink/77-mm-tplink-port-types.rules
+
+AM_CFLAGS += -DTESTUDEVRULESDIR_TPLINK=\"${srcdir}/tplink\"
+
+################################################################################
 # udev rules tester
 ################################################################################
 
diff --git a/plugins/altair/mm-broadband-bearer-altair-lte.c b/plugins/altair/mm-broadband-bearer-altair-lte.c
index bde06e8..d8e1a0f 100644
--- a/plugins/altair/mm-broadband-bearer-altair-lte.c
+++ b/plugins/altair/mm-broadband-bearer-altair-lte.c
@@ -219,11 +219,9 @@
 /* 3GPP Disconnect sequence */
 
 typedef struct {
-    MMBroadbandBearer *self;
     MMBaseModem *modem;
     MMPortSerialAt *primary;
     MMPort *data;
-    GSimpleAsyncResult *result;
 } DetailedDisconnectContext;
 
 static gboolean
diff --git a/plugins/cinterion/77-mm-cinterion-port-types.rules b/plugins/cinterion/77-mm-cinterion-port-types.rules
index 89a792c..e21a1b1 100644
--- a/plugins/cinterion/77-mm-cinterion-port-types.rules
+++ b/plugins/cinterion/77-mm-cinterion-port-types.rules
@@ -10,7 +10,14 @@
 # PHS8
 ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0053", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_GPS}="1"
 
-# PLS8
+# PLS8 port types
+#  ttyACM0 (if #0): AT port
+#  ttyACM1 (if #2): AT port
+#  ttyACM2 (if #4): GPS data port
+#  ttyACM3 (if #6): unknown
+#  ttyACM4 (if #8): unknown
 ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_TYPE_GPS}="1"
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="08", ENV{ID_MM_PORT_IGNORE}="1"
 
 LABEL="mm_cinterion_port_types_end"
diff --git a/plugins/cinterion/mm-broadband-modem-cinterion.c b/plugins/cinterion/mm-broadband-modem-cinterion.c
index 14ff248..a99db32 100644
--- a/plugins/cinterion/mm-broadband-modem-cinterion.c
+++ b/plugins/cinterion/mm-broadband-modem-cinterion.c
@@ -34,6 +34,7 @@
 #include "mm-iface-modem-3gpp.h"
 #include "mm-iface-modem-messaging.h"
 #include "mm-iface-modem-location.h"
+#include "mm-iface-modem-voice.h"
 #include "mm-base-modem-at.h"
 #include "mm-broadband-modem-cinterion.h"
 #include "mm-modem-helpers-cinterion.h"
@@ -44,17 +45,23 @@
 static void iface_modem_3gpp_init      (MMIfaceModem3gpp      *iface);
 static void iface_modem_messaging_init (MMIfaceModemMessaging *iface);
 static void iface_modem_location_init  (MMIfaceModemLocation  *iface);
+static void iface_modem_voice_init     (MMIfaceModemVoice     *iface);
+static void iface_modem_time_init      (MMIfaceModemTime      *iface);
 static void shared_cinterion_init      (MMSharedCinterion     *iface);
 
 static MMIfaceModem         *iface_modem_parent;
 static MMIfaceModem3gpp     *iface_modem_3gpp_parent;
 static MMIfaceModemLocation *iface_modem_location_parent;
+static MMIfaceModemVoice    *iface_modem_voice_parent;
+static MMIfaceModemTime     *iface_modem_time_parent;
 
 G_DEFINE_TYPE_EXTENDED (MMBroadbandModemCinterion, mm_broadband_modem_cinterion, MM_TYPE_BROADBAND_MODEM, 0,
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM, iface_modem_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_3GPP, iface_modem_3gpp_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_MESSAGING, iface_modem_messaging_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_LOCATION, iface_modem_location_init)
+                        G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_VOICE, iface_modem_voice_init)
+                        G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_TIME, iface_modem_time_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_SHARED_CINTERION, shared_cinterion_init))
 
 typedef enum {
@@ -80,8 +87,8 @@
     GArray *cnmi_supported_ds;
     GArray *cnmi_supported_bfr;
 
-    /* +CIEV 'psinfo' indications */
-    GRegex *ciev_psinfo_regex;
+    /* +CIEV indications as configured via AT^SIND */
+    GRegex *ciev_regex;
 
     /* Flags for feature support checks */
     FeatureSupport swwan_support;
@@ -290,6 +297,30 @@
 }
 
 /*****************************************************************************/
+/* Reset (Modem interface) */
+
+static gboolean
+modem_reset_finish (MMIfaceModem  *self,
+                    GAsyncResult  *res,
+                    GError       **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_reset (MMIfaceModem        *self,
+             GAsyncReadyCallback  callback,
+             gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CFUN=1,1",
+                              3,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
 /* Power down */
 
 static gboolean
@@ -814,20 +845,25 @@
 /* Setup/Cleanup unsolicited events (3GPP interface) */
 
 static void
-sind_psinfo_received (MMPortSerialAt            *port,
-                      GMatchInfo                *match_info,
-                      MMBroadbandModemCinterion *self)
+sind_ciev_received (MMPortSerialAt            *port,
+                    GMatchInfo                *match_info,
+                    MMBroadbandModemCinterion *self)
 {
-    guint val;
+    guint  val = 0;
+    gchar *indicator;
 
-    if (!mm_get_uint_from_match_info (match_info, 1, &val)) {
-        mm_dbg ("Failed to convert psinfo value");
-        return;
+    indicator = mm_get_string_unquoted_from_match_info (match_info, 1);
+    if (!mm_get_uint_from_match_info (match_info, 2, &val))
+        mm_dbg ("couldn't parse indicator '%s' value", indicator);
+    else {
+        mm_dbg ("received indicator '%s' update: %u", indicator, val);
+        if (g_strcmp0 (indicator, "psinfo") == 0) {
+            mm_iface_modem_update_access_technologies (MM_IFACE_MODEM (self),
+                                                       mm_cinterion_get_access_technology_from_sind_psinfo (val),
+                                                       MM_IFACE_MODEM_3GPP_ALL_ACCESS_TECHNOLOGIES_MASK);
+        }
     }
-
-    mm_iface_modem_update_access_technologies (MM_IFACE_MODEM (self),
-                                               mm_cinterion_get_access_technology_from_sind_psinfo (val),
-                                               MM_IFACE_MODEM_3GPP_ALL_ACCESS_TECHNOLOGIES_MASK);
+    g_free (indicator);
 }
 
 static void
@@ -847,8 +883,8 @@
 
         mm_port_serial_at_add_unsolicited_msg_handler (
             ports[i],
-            self->priv->ciev_psinfo_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)sind_psinfo_received : NULL,
+            self->priv->ciev_regex,
+            enable ? (MMPortSerialAtUnsolicitedMsgFn)sind_ciev_received : NULL,
             enable ? self : NULL,
             NULL);
     }
@@ -1819,8 +1855,8 @@
     self->priv->sind_psinfo_support = FEATURE_SUPPORT_UNKNOWN;
     self->priv->swwan_support       = FEATURE_SUPPORT_UNKNOWN;
 
-    self->priv->ciev_psinfo_regex = g_regex_new ("\\r\\n\\+CIEV: psinfo,(\\d+)\\r\\n",
-                                                 G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    self->priv->ciev_regex = g_regex_new ("\\r\\n\\+CIEV:\\s*([a-z]+),(\\d+)\\r\\n",
+                                          G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
 }
 
 static void
@@ -1842,7 +1878,7 @@
     if (self->priv->cnmi_supported_bfr)
         g_array_unref (self->priv->cnmi_supported_bfr);
 
-    g_regex_unref (self->priv->ciev_psinfo_regex);
+    g_regex_unref (self->priv->ciev_regex);
 
     G_OBJECT_CLASS (mm_broadband_modem_cinterion_parent_class)->finalize (object);
 }
@@ -1872,6 +1908,8 @@
     iface->modem_after_sim_unlock_finish = after_sim_unlock_finish;
     iface->load_unlock_retries = load_unlock_retries;
     iface->load_unlock_retries_finish = load_unlock_retries_finish;
+    iface->reset = modem_reset;
+    iface->reset_finish = modem_reset_finish;
     iface->modem_power_down = modem_power_down;
     iface->modem_power_down_finish = modem_power_down_finish;
     iface->modem_power_off = modem_power_off;
@@ -1926,9 +1964,53 @@
 }
 
 static void
+iface_modem_voice_init (MMIfaceModemVoice *iface)
+{
+    iface_modem_voice_parent = g_type_interface_peek_parent (iface);
+
+    iface->create_call = mm_shared_cinterion_create_call;
+
+    iface->check_support                     = mm_shared_cinterion_voice_check_support;
+    iface->check_support_finish              = mm_shared_cinterion_voice_check_support_finish;
+    iface->enable_unsolicited_events         = mm_shared_cinterion_voice_enable_unsolicited_events;
+    iface->enable_unsolicited_events_finish  = mm_shared_cinterion_voice_enable_unsolicited_events_finish;
+    iface->disable_unsolicited_events        = mm_shared_cinterion_voice_disable_unsolicited_events;
+    iface->disable_unsolicited_events_finish = mm_shared_cinterion_voice_disable_unsolicited_events_finish;
+    iface->setup_unsolicited_events          = mm_shared_cinterion_voice_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish   = mm_shared_cinterion_voice_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events        = mm_shared_cinterion_voice_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = mm_shared_cinterion_voice_cleanup_unsolicited_events_finish;
+}
+
+static MMIfaceModemVoice *
+peek_parent_voice_interface (MMSharedCinterion *self)
+{
+    return iface_modem_voice_parent;
+}
+
+static void
+iface_modem_time_init (MMIfaceModemTime *iface)
+{
+    iface_modem_time_parent = g_type_interface_peek_parent (iface);
+
+    iface->setup_unsolicited_events          = mm_shared_cinterion_time_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish   = mm_shared_cinterion_time_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events        = mm_shared_cinterion_time_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = mm_shared_cinterion_time_cleanup_unsolicited_events_finish;
+}
+
+static MMIfaceModemTime *
+peek_parent_time_interface (MMSharedCinterion *self)
+{
+    return iface_modem_time_parent;
+}
+
+static void
 shared_cinterion_init (MMSharedCinterion *iface)
 {
     iface->peek_parent_location_interface = peek_parent_location_interface;
+    iface->peek_parent_voice_interface    = peek_parent_voice_interface;
+    iface->peek_parent_time_interface     = peek_parent_time_interface;
 }
 
 static void
diff --git a/plugins/cinterion/mm-broadband-modem-qmi-cinterion.c b/plugins/cinterion/mm-broadband-modem-qmi-cinterion.c
index 6048cca..c14fb23 100644
--- a/plugins/cinterion/mm-broadband-modem-qmi-cinterion.c
+++ b/plugins/cinterion/mm-broadband-modem-qmi-cinterion.c
@@ -26,16 +26,23 @@
 #include "mm-log.h"
 #include "mm-errors-types.h"
 #include "mm-iface-modem-location.h"
+#include "mm-iface-modem-voice.h"
 #include "mm-broadband-modem-qmi-cinterion.h"
 #include "mm-shared-cinterion.h"
 
 static void iface_modem_location_init (MMIfaceModemLocation *iface);
+static void iface_modem_voice_init    (MMIfaceModemVoice    *iface);
+static void iface_modem_time_init     (MMIfaceModemTime     *iface);
 static void shared_cinterion_init     (MMSharedCinterion    *iface);
 
 static MMIfaceModemLocation *iface_modem_location_parent;
+static MMIfaceModemVoice    *iface_modem_voice_parent;
+static MMIfaceModemTime     *iface_modem_time_parent;
 
 G_DEFINE_TYPE_EXTENDED (MMBroadbandModemQmiCinterion, mm_broadband_modem_qmi_cinterion, MM_TYPE_BROADBAND_MODEM_QMI, 0,
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_LOCATION, iface_modem_location_init)
+                        G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_VOICE, iface_modem_voice_init)
+                        G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_TIME, iface_modem_time_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_SHARED_CINTERION, shared_cinterion_init))
 
 /*****************************************************************************/
@@ -81,9 +88,53 @@
 }
 
 static void
+iface_modem_voice_init (MMIfaceModemVoice *iface)
+{
+    iface_modem_voice_parent = g_type_interface_peek_parent (iface);
+
+    iface->create_call = mm_shared_cinterion_create_call;
+
+    iface->check_support                     = mm_shared_cinterion_voice_check_support;
+    iface->check_support_finish              = mm_shared_cinterion_voice_check_support_finish;
+    iface->enable_unsolicited_events         = mm_shared_cinterion_voice_enable_unsolicited_events;
+    iface->enable_unsolicited_events_finish  = mm_shared_cinterion_voice_enable_unsolicited_events_finish;
+    iface->disable_unsolicited_events        = mm_shared_cinterion_voice_disable_unsolicited_events;
+    iface->disable_unsolicited_events_finish = mm_shared_cinterion_voice_disable_unsolicited_events_finish;
+    iface->setup_unsolicited_events          = mm_shared_cinterion_voice_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish   = mm_shared_cinterion_voice_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events        = mm_shared_cinterion_voice_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = mm_shared_cinterion_voice_cleanup_unsolicited_events_finish;
+}
+
+static MMIfaceModemVoice *
+peek_parent_voice_interface (MMSharedCinterion *self)
+{
+    return iface_modem_voice_parent;
+}
+
+static void
+iface_modem_time_init (MMIfaceModemTime *iface)
+{
+    iface_modem_time_parent = g_type_interface_peek_parent (iface);
+
+    iface->setup_unsolicited_events          = mm_shared_cinterion_time_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish   = mm_shared_cinterion_time_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events        = mm_shared_cinterion_time_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = mm_shared_cinterion_time_cleanup_unsolicited_events_finish;
+}
+
+static MMIfaceModemTime *
+peek_parent_time_interface (MMSharedCinterion *self)
+{
+    return iface_modem_time_parent;
+}
+
+static void
 shared_cinterion_init (MMSharedCinterion *iface)
 {
     iface->peek_parent_location_interface = peek_parent_location_interface;
+    iface->peek_parent_voice_interface    = peek_parent_voice_interface;
+    iface->peek_parent_time_interface     = peek_parent_time_interface;
 }
 
 static void
diff --git a/plugins/cinterion/mm-modem-helpers-cinterion.c b/plugins/cinterion/mm-modem-helpers-cinterion.c
index 2afd11a..0eac912 100644
--- a/plugins/cinterion/mm-modem-helpers-cinterion.c
+++ b/plugins/cinterion/mm-modem-helpers-cinterion.c
@@ -10,9 +10,10 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2016 Trimble Navigation Limited
  * Copyright (C) 2014 Aleksander Morgado <aleksander@aleksander.es>
- * Contributor: Matthew Stanger <matthew_stanger@trimble.com>
+ * Copyright (C) 2016 Trimble Navigation Limited
+ * Copyright (C) 2016 Matthew Stanger <matthew_stanger@trimble.com>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #include <config.h>
@@ -201,7 +202,7 @@
     r = g_regex_new ("\\^SCFG:\\s*\"Radio/Band\",\\s*\"?([0-9a-fA-F]*)\"?", 0, 0, NULL);
     g_assert (r != NULL);
 
-    if (g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, response, 0, &match_info)) {
         gchar *currentstr;
         guint current = 0;
 
@@ -441,7 +442,7 @@
     r = g_regex_new ("\\^SIND:\\s*(.*),(\\d+),(\\d+)(\\r\\n)?", 0, 0, NULL);
     g_assert (r != NULL);
 
-    if (g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, response, 0, &match_info)) {
         if (description) {
             *description = mm_get_string_unquoted_from_match_info (match_info, 1);
             if (*description == NULL)
@@ -667,3 +668,201 @@
         return MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;
     }
 }
+
+/*****************************************************************************/
+/* ^SLCC psinfo helper */
+
+GRegex *
+mm_cinterion_get_slcc_regex (void)
+{
+    /* The list of active calls displayed with this URC will always be terminated
+     * with an empty line preceded by prefix "^SLCC: ", in order to indicate the end
+     * of the list.
+     */
+    return g_regex_new ("\\r\\n(\\^SLCC: .*\\r\\n)*\\^SLCC: \\r\\n",
+                        G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+}
+
+static void
+cinterion_call_info_free (MMCallInfo *info)
+{
+    if (!info)
+        return;
+    g_free (info->number);
+    g_slice_free (MMCallInfo, info);
+}
+
+gboolean
+mm_cinterion_parse_slcc_list (const gchar *str,
+                              GList      **out_list,
+                              GError     **error)
+{
+    GRegex     *r;
+    GList      *list = NULL;
+    GError     *inner_error = NULL;
+    GMatchInfo *match_info  = NULL;
+
+    static const MMCallDirection cinterion_call_direction[] = {
+        [0] = MM_CALL_DIRECTION_OUTGOING,
+        [1] = MM_CALL_DIRECTION_INCOMING,
+    };
+
+    static const MMCallState cinterion_call_state[] = {
+        [0] = MM_CALL_STATE_ACTIVE,
+        [1] = MM_CALL_STATE_HELD,
+        [2] = MM_CALL_STATE_DIALING,     /* Dialing  (MOC) */
+        [3] = MM_CALL_STATE_RINGING_OUT, /* Alerting (MOC) */
+        [4] = MM_CALL_STATE_RINGING_IN,  /* Incoming (MTC) */
+        [5] = MM_CALL_STATE_WAITING,     /* Waiting  (MTC) */
+    };
+
+    g_assert (out_list);
+
+    /*
+     *         1      2      3       4       5       6            7         8     9
+     *  ^SLCC: <idx>, <dir>, <stat>, <mode>, <mpty>, <Reserved>[, <number>, <type>[,<alpha>]]
+     *  [^SLCC: <idx>, <dir>, <stat>, <mode>, <mpty>, <Reserved>[, <number>, <type>[,<alpha>]]]
+     *  [... ]
+     *  ^SLCC :
+     */
+
+    r = g_regex_new ("\\^SLCC:\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+)" /* mandatory fields */
+                     "(?:,\\s*([^,]*),\\s*(\\d+)"                                                /* number and type */
+                     "(?:,\\s*([^,]*)"                                                           /* alpha */
+                     ")?)?$",
+                     G_REGEX_RAW | G_REGEX_MULTILINE | G_REGEX_NEWLINE_CRLF,
+                     G_REGEX_MATCH_NEWLINE_CRLF,
+                     NULL);
+    g_assert (r != NULL);
+
+    g_regex_match_full (r, str, strlen (str), 0, 0, &match_info, &inner_error);
+    if (inner_error)
+        goto out;
+
+    /* Parse the results */
+    while (g_match_info_matches (match_info)) {
+        MMCallInfo *call_info;
+        guint       aux;
+
+        call_info = g_slice_new0 (MMCallInfo);
+
+        if (!mm_get_uint_from_match_info (match_info, 1, &call_info->index)) {
+            mm_warn ("couldn't parse call index from ^SLCC line");
+            goto next;
+        }
+
+        if (!mm_get_uint_from_match_info (match_info, 2, &aux) ||
+            (aux >= G_N_ELEMENTS (cinterion_call_direction))) {
+            mm_warn ("couldn't parse call direction from ^SLCC line");
+            goto next;
+        }
+        call_info->direction = cinterion_call_direction[aux];
+
+        if (!mm_get_uint_from_match_info (match_info, 3, &aux) ||
+            (aux >= G_N_ELEMENTS (cinterion_call_state))) {
+            mm_warn ("couldn't parse call state from ^SLCC line");
+            goto next;
+        }
+        call_info->state = cinterion_call_state[aux];
+
+        if (g_match_info_get_match_count (match_info) >= 8)
+            call_info->number = mm_get_string_unquoted_from_match_info (match_info, 7);
+
+        list = g_list_append (list, call_info);
+        call_info = NULL;
+
+    next:
+        cinterion_call_info_free (call_info);
+        g_match_info_next (match_info, NULL);
+    }
+
+out:
+    g_clear_pointer (&match_info, g_match_info_free);
+    g_regex_unref (r);
+
+    if (inner_error) {
+        mm_cinterion_call_info_list_free (list);
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    *out_list = list;
+
+    return TRUE;
+}
+
+void
+mm_cinterion_call_info_list_free (GList *call_info_list)
+{
+    g_list_free_full (call_info_list, (GDestroyNotify) cinterion_call_info_free);
+}
+
+/*****************************************************************************/
+/* +CTZU URC helpers */
+
+GRegex *
+mm_cinterion_get_ctzu_regex (void)
+{
+    /*
+     * From PLS-8 AT command spec:
+     *  +CTZU:<nitzUT>, <nitzTZ>[, <nitzDST>]
+     * E.g.:
+     *  +CTZU: "19/07/09,10:19:15",+08,1
+     */
+
+    return g_regex_new ("\\r\\n\\+CTZU:\\s*\"(\\d+)\\/(\\d+)\\/(\\d+),(\\d+):(\\d+):(\\d+)\",([\\-\\+\\d]+)(?:,(\\d+))?(?:\\r\\n)?",
+                        G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+}
+
+gboolean
+mm_cinterion_parse_ctzu_urc (GMatchInfo         *match_info,
+                             gchar             **iso8601p,
+                             MMNetworkTimezone **tzp,
+                             GError            **error)
+{
+    guint year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, dst = 0;
+    gint tz = 0;
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &year)   ||
+        !mm_get_uint_from_match_info (match_info, 2, &month)  ||
+        !mm_get_uint_from_match_info (match_info, 3, &day)    ||
+        !mm_get_uint_from_match_info (match_info, 4, &hour)   ||
+        !mm_get_uint_from_match_info (match_info, 5, &minute) ||
+        !mm_get_uint_from_match_info (match_info, 6, &second) ||
+        !mm_get_int_from_match_info  (match_info, 7, &tz)) {
+        g_set_error_literal (error,
+                             MM_CORE_ERROR,
+                             MM_CORE_ERROR_FAILED,
+                             "Failed to parse +CTZU URC");
+        return FALSE;
+    }
+
+    /* adjust year */
+    if (year < 100)
+        year += 2000;
+
+    /*
+     * tz = timezone offset in 15 minute intervals
+     */
+    if (iso8601p) {
+        /* Return ISO-8601 format date/time string */
+        *iso8601p = mm_new_iso8601_time (year, month, day, hour,
+                                         minute, second,
+                                         TRUE, tz * 15);
+    }
+
+    if (tzp) {
+        *tzp = mm_network_timezone_new ();
+        mm_network_timezone_set_offset (*tzp, tz * 15);
+    }
+
+    /* dst flag is optional in the URC
+    *
+     * tz = timezone offset in 15 minute intervals
+     * dst = daylight adjustment, 0 = none, 1 = 1 hour, 2 = 2 hours
+     */
+    if (tzp && mm_get_uint_from_match_info (match_info, 8, &dst))
+        mm_network_timezone_set_dst_offset (*tzp, dst * 60);
+
+    return TRUE;
+}
diff --git a/plugins/cinterion/mm-modem-helpers-cinterion.h b/plugins/cinterion/mm-modem-helpers-cinterion.h
index 2ec0515..a318b8d 100644
--- a/plugins/cinterion/mm-modem-helpers-cinterion.h
+++ b/plugins/cinterion/mm-modem-helpers-cinterion.h
@@ -10,9 +10,10 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2016 Trimble Navigation Limited
  * Copyright (C) 2014 Aleksander Morgado <aleksander@aleksander.es>
- * Contributor: Matthew Stanger <matthew_stanger@trimble.com>
+ * Copyright (C) 2016 Trimble Navigation Limited
+ * Copyright (C) 2016 Matthew Stanger <matthew_stanger@trimble.com>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #ifndef MM_MODEM_HELPERS_CINTERION_H
@@ -22,6 +23,8 @@
 
 #include <ModemManager.h>
 #include <mm-base-bearer.h>
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
 
 /*****************************************************************************/
 /* ^SCFG test parser */
@@ -87,4 +90,24 @@
 
 MMModemAccessTechnology mm_cinterion_get_access_technology_from_sind_psinfo (guint val);
 
+/*****************************************************************************/
+/* ^SLCC URC helpers */
+
+GRegex *mm_cinterion_get_slcc_regex (void);
+
+/* MMCallInfo list management */
+gboolean mm_cinterion_parse_slcc_list     (const gchar  *str,
+                                           GList       **out_list,
+                                           GError      **error);
+void     mm_cinterion_call_info_list_free (GList        *call_info_list);
+
+/*****************************************************************************/
+/* +CTZU URC helpers */
+
+GRegex   *mm_cinterion_get_ctzu_regex (void);
+gboolean  mm_cinterion_parse_ctzu_urc (GMatchInfo         *match_info,
+                                       gchar             **iso8601p,
+                                       MMNetworkTimezone **tzp,
+                                       GError            **error);
+
 #endif  /* MM_MODEM_HELPERS_CINTERION_H */
diff --git a/plugins/cinterion/mm-shared-cinterion.c b/plugins/cinterion/mm-shared-cinterion.c
index ab95140..5cb35a8 100644
--- a/plugins/cinterion/mm-shared-cinterion.c
+++ b/plugins/cinterion/mm-shared-cinterion.c
@@ -12,6 +12,7 @@
  *
  * Copyright (C) 2014 Ammonit Measurement GmbH
  * Copyright (C) 2014 - 2018 Aleksander Morgado <aleksander@aleksander.es>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #include <config.h>
@@ -28,6 +29,7 @@
 #include "mm-base-modem.h"
 #include "mm-base-modem-at.h"
 #include "mm-shared-cinterion.h"
+#include "mm-modem-helpers-cinterion.h"
 
 /*****************************************************************************/
 /* Private data context */
@@ -42,16 +44,26 @@
 } FeatureSupport;
 
 typedef struct {
+    /* location */
     MMIfaceModemLocation  *iface_modem_location_parent;
     MMModemLocationSource  supported_sources;
     MMModemLocationSource  enabled_sources;
     FeatureSupport         sgpss_support;
     FeatureSupport         sgpsc_support;
+    /* voice */
+    MMIfaceModemVoice     *iface_modem_voice_parent;
+    FeatureSupport         slcc_support;
+    GRegex                *slcc_regex;
+    /* time */
+    MMIfaceModemTime      *iface_modem_time_parent;
+    GRegex                *ctzu_regex;
 } Private;
 
 static void
 private_free (Private *ctx)
 {
+    g_regex_unref (ctx->ctzu_regex);
+    g_regex_unref (ctx->slcc_regex);
     g_slice_free (Private, ctx);
 }
 
@@ -71,11 +83,21 @@
         priv->enabled_sources = MM_MODEM_LOCATION_SOURCE_NONE;
         priv->sgpss_support = FEATURE_SUPPORT_UNKNOWN;
         priv->sgpsc_support = FEATURE_SUPPORT_UNKNOWN;
+        priv->slcc_support = FEATURE_SUPPORT_UNKNOWN;
+        priv->slcc_regex = mm_cinterion_get_slcc_regex ();
+        priv->ctzu_regex = mm_cinterion_get_ctzu_regex ();
 
-        /* Setup parent class' MMIfaceModemLocation */
+        /* Setup parent class' MMIfaceModemLocation, MMIfaceModemVoice and MMIfaceModemTime */
+
         g_assert (MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_location_interface);
         priv->iface_modem_location_parent = MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_location_interface (self);
 
+        g_assert (MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_voice_interface);
+        priv->iface_modem_voice_parent = MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_voice_interface (self);
+
+        g_assert (MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_time_interface);
+        priv->iface_modem_time_parent = MM_SHARED_CINTERION_GET_INTERFACE (self)->peek_parent_time_interface (self);
+
         g_object_set_qdata_full (G_OBJECT (self), private_quark, priv, (GDestroyNotify)private_free);
     }
 
@@ -787,6 +809,726 @@
 
 /*****************************************************************************/
 
+MMBaseCall *
+mm_shared_cinterion_create_call (MMIfaceModemVoice *self,
+                                 MMCallDirection    direction,
+                                 const gchar       *number)
+{
+    Private *priv;
+
+    /* If ^SLCC is supported create a cinterion call object */
+    priv = get_private (MM_SHARED_CINTERION (self));
+    if (priv->slcc_support == FEATURE_SUPPORTED) {
+        mm_dbg ("Created new call with ^SLCC support");
+        return mm_base_call_new (MM_BASE_MODEM (self),
+                                 direction,
+                                 number,
+                                 /* When SLCC is supported we have support for detailed
+                                  * call list events via call list report URCs */
+                                 TRUE,   /* incoming timeout not required */
+                                 TRUE,   /* dialing->ringing supported */
+                                 TRUE);  /* ringing->active supported */
+    }
+
+    /* otherwise, run parent's generic base call logic */
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->create_call);
+    return priv->iface_modem_voice_parent->create_call (self, direction, number);
+}
+
+/*****************************************************************************/
+/* Common enable/disable voice unsolicited events */
+
+typedef struct {
+    gboolean        enable;
+    MMPortSerialAt *primary;
+    MMPortSerialAt *secondary;
+    gchar          *slcc_command;
+    gboolean        slcc_primary_done;
+    gboolean        slcc_secondary_done;
+} VoiceUnsolicitedEventsContext;
+
+static void
+voice_unsolicited_events_context_free (VoiceUnsolicitedEventsContext *ctx)
+{
+    g_clear_object (&ctx->secondary);
+    g_clear_object (&ctx->primary);
+    g_free (ctx->slcc_command);
+    g_slice_free (VoiceUnsolicitedEventsContext, ctx);
+}
+
+static gboolean
+common_voice_enable_disable_unsolicited_events_finish (MMSharedCinterion  *self,
+                                                       GAsyncResult       *res,
+                                                       GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void run_voice_enable_disable_unsolicited_events (GTask *task);
+
+static void
+slcc_command_ready (MMBaseModem  *self,
+                    GAsyncResult *res,
+                    GTask        *task)
+{
+    VoiceUnsolicitedEventsContext *ctx;
+    GError                        *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!mm_base_modem_at_command_finish (self, res, &error)) {
+        mm_dbg ("Couldn't %s ^SLCC reporting: '%s'",
+                ctx->enable ? "enable" : "disable",
+                error->message);
+        g_error_free (error);
+    }
+
+    /* Continue on next port */
+    run_voice_enable_disable_unsolicited_events (task);
+}
+
+static void
+run_voice_enable_disable_unsolicited_events (GTask *task)
+{
+    MMSharedCinterion             *self;
+    Private                       *priv;
+    VoiceUnsolicitedEventsContext *ctx;
+    MMPortSerialAt                *port = NULL;
+
+    self = MM_SHARED_CINTERION (g_task_get_source_object (task));
+    priv = get_private (self);
+    ctx  = g_task_get_task_data (task);
+
+    /* If not ^SLCC supported, we're done */
+    if (priv->slcc_support == FEATURE_NOT_SUPPORTED) {
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    if (!ctx->slcc_primary_done && ctx->primary) {
+        mm_dbg ("%s ^SLCC  extended list of current calls reporting in primary port...",
+                ctx->enable ? "Enabling" : "Disabling");
+        ctx->slcc_primary_done = TRUE;
+        port = ctx->primary;
+    } else if (!ctx->slcc_secondary_done && ctx->secondary) {
+        mm_dbg ("%s ^SLCC  extended list of current calls reporting in secondary port...",
+                ctx->enable ? "Enabling" : "Disabling");
+        ctx->slcc_secondary_done = TRUE;
+        port = ctx->secondary;
+    }
+
+    if (port) {
+        mm_base_modem_at_command_full (MM_BASE_MODEM (self),
+                                       port,
+                                       ctx->slcc_command,
+                                       3,
+                                       FALSE,
+                                       FALSE,
+                                       NULL,
+                                       (GAsyncReadyCallback)slcc_command_ready,
+                                       task);
+        return;
+    }
+
+    /* Fully done now */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+common_voice_enable_disable_unsolicited_events (MMSharedCinterion   *self,
+                                                gboolean             enable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+    VoiceUnsolicitedEventsContext *ctx;
+    GTask                         *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    ctx = g_slice_new0 (VoiceUnsolicitedEventsContext);
+    ctx->enable = enable;
+    if (enable)
+        ctx->slcc_command = g_strdup ("^SLCC=1");
+    else
+        ctx->slcc_command = g_strdup ("^SLCC=0");
+    ctx->primary = mm_base_modem_get_port_primary (MM_BASE_MODEM (self));
+    ctx->secondary = mm_base_modem_get_port_secondary (MM_BASE_MODEM (self));
+    g_task_set_task_data (task, ctx, (GDestroyNotify) voice_unsolicited_events_context_free);
+
+    run_voice_enable_disable_unsolicited_events (task);
+}
+
+/*****************************************************************************/
+/* Disable unsolicited events (Voice interface) */
+
+gboolean
+mm_shared_cinterion_voice_disable_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                             GAsyncResult       *res,
+                                                             GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_disable_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_voice_parent->disable_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't disable parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+voice_disable_unsolicited_events_ready (MMSharedCinterion *self,
+                                        GAsyncResult      *res,
+                                        GTask             *task)
+{
+    Private *priv;
+    GError  *error = NULL;
+
+    if (!common_voice_enable_disable_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't disable Cinterion-specific voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->disable_unsolicited_events);
+    g_assert (priv->iface_modem_voice_parent->disable_unsolicited_events_finish);
+
+    /* Chain up parent's disable */
+    priv->iface_modem_voice_parent->disable_unsolicited_events (
+        MM_IFACE_MODEM_VOICE (self),
+        (GAsyncReadyCallback)parent_voice_disable_unsolicited_events_ready,
+        task);
+}
+
+void
+mm_shared_cinterion_voice_disable_unsolicited_events (MMIfaceModemVoice   *self,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* our own disabling first */
+    common_voice_enable_disable_unsolicited_events (MM_SHARED_CINTERION (self),
+                                                    FALSE,
+                                                    (GAsyncReadyCallback) voice_disable_unsolicited_events_ready,
+                                                    task);
+}
+
+/*****************************************************************************/
+/* Enable unsolicited events (Voice interface) */
+
+gboolean
+mm_shared_cinterion_voice_enable_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                            GAsyncResult       *res,
+                                                            GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+voice_enable_unsolicited_events_ready (MMSharedCinterion *self,
+                                       GAsyncResult      *res,
+                                       GTask             *task)
+{
+    GError *error = NULL;
+
+    if (!common_voice_enable_disable_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't enable Cinterion-specific voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+parent_voice_enable_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                              GAsyncResult      *res,
+                                              GTask             *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_voice_parent->enable_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't enable parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    /* our own enabling next */
+    common_voice_enable_disable_unsolicited_events (MM_SHARED_CINTERION (self),
+                                                    TRUE,
+                                                    (GAsyncReadyCallback) voice_enable_unsolicited_events_ready,
+                                                    task);
+}
+
+void
+mm_shared_cinterion_voice_enable_unsolicited_events (MMIfaceModemVoice   *self,
+                                                     GAsyncReadyCallback  callback,
+                                                     gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->enable_unsolicited_events);
+    g_assert (priv->iface_modem_voice_parent->enable_unsolicited_events_finish);
+
+    /* chain up parent's enable first */
+    priv->iface_modem_voice_parent->enable_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_enable_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Common setup/cleanup voice unsolicited events */
+
+static void
+slcc_received (MMPortSerialAt    *port,
+               GMatchInfo        *match_info,
+               MMSharedCinterion *self)
+{
+    gchar  *full;
+    GError *error = NULL;
+    GList  *call_info_list = NULL;
+
+    full = g_match_info_fetch (match_info, 0);
+
+    if (!mm_cinterion_parse_slcc_list (full, &call_info_list, &error)) {
+        mm_warn ("couldn't parse ^SLCC list: %s", error->message);
+        g_error_free (error);
+    } else
+        mm_iface_modem_voice_report_all_calls (MM_IFACE_MODEM_VOICE (self), call_info_list);
+
+    mm_cinterion_call_info_list_free (call_info_list);
+    g_free (full);
+}
+
+static void
+common_voice_setup_cleanup_unsolicited_events (MMSharedCinterion *self,
+                                               gboolean           enable)
+{
+    Private        *priv;
+    MMPortSerialAt *ports[2];
+    guint           i;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    ports[0] = mm_base_modem_peek_port_primary   (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
+        if (!ports[i])
+            continue;
+
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       priv->slcc_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)slcc_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+    }
+}
+
+/*****************************************************************************/
+/* Cleanup unsolicited events (Voice interface) */
+
+gboolean
+mm_shared_cinterion_voice_cleanup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                             GAsyncResult       *res,
+                                                             GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_cleanup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_voice_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_shared_cinterion_voice_cleanup_unsolicited_events (MMIfaceModemVoice   *self,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->cleanup_unsolicited_events);
+    g_assert (priv->iface_modem_voice_parent->cleanup_unsolicited_events_finish);
+
+    /* our own cleanup first */
+    common_voice_setup_cleanup_unsolicited_events (MM_SHARED_CINTERION (self), FALSE);
+
+    /* Chain up parent's cleanup */
+    priv->iface_modem_voice_parent->cleanup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_cleanup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Setup unsolicited events (Voice interface) */
+
+gboolean
+mm_shared_cinterion_voice_setup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                           GAsyncResult       *res,
+                                                           GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_setup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                             GAsyncResult      *res,
+                                             GTask             *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_voice_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    /* our own setup next */
+    common_voice_setup_cleanup_unsolicited_events (MM_SHARED_CINTERION (self), TRUE);
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_shared_cinterion_voice_setup_unsolicited_events (MMIfaceModemVoice   *self,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->setup_unsolicited_events);
+    g_assert (priv->iface_modem_voice_parent->setup_unsolicited_events_finish);
+
+    /* chain up parent's setup first */
+    priv->iface_modem_voice_parent->setup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_setup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Check if Voice supported (Voice interface) */
+
+gboolean
+mm_shared_cinterion_voice_check_support_finish (MMIfaceModemVoice  *self,
+                                                GAsyncResult       *res,
+                                                GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+slcc_format_check_ready (MMBroadbandModem *self,
+                         GAsyncResult     *res,
+                         GTask            *task)
+{
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    /* ^SLCC supported unless we got any error response */
+    priv->slcc_support = (!!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, NULL) ?
+                          FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED);
+
+    /* If ^SLCC supported we won't need polling in the parent */
+    g_object_set (self,
+                  MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED, (priv->slcc_support == FEATURE_SUPPORTED),
+                  NULL);
+
+    /* ^SLCC command is supported; assume we have full voice capabilities */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+parent_voice_check_support_ready (MMIfaceModemVoice *self,
+                                  GAsyncResult      *res,
+                                  GTask             *task)
+{
+    Private *priv;
+    GError  *error = NULL;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    if (!priv->iface_modem_voice_parent->check_support_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* voice is supported, check if ^SLCC is available */
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "^SLCC=?",
+                              3,
+                              TRUE,
+                              (GAsyncReadyCallback) slcc_format_check_ready,
+                              task);
+}
+
+void
+mm_shared_cinterion_voice_check_support (MMIfaceModemVoice   *self,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_voice_parent);
+    g_assert (priv->iface_modem_voice_parent->check_support);
+    g_assert (priv->iface_modem_voice_parent->check_support_finish);
+
+    /* chain up parent's setup first */
+    priv->iface_modem_voice_parent->check_support (
+        self,
+        (GAsyncReadyCallback)parent_voice_check_support_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Common setup/cleanup time unsolicited events */
+
+static void
+ctzu_received (MMPortSerialAt    *port,
+               GMatchInfo        *match_info,
+               MMSharedCinterion *self)
+{
+    gchar             *iso8601 = NULL;
+    MMNetworkTimezone *tz = NULL;
+    GError            *error = NULL;
+
+    if (!mm_cinterion_parse_ctzu_urc (match_info, &iso8601, &tz, &error)) {
+        mm_dbg ("Couldn't process +CTZU URC: %s", error->message);
+        g_error_free (error);
+        return;
+    }
+
+    g_assert (iso8601);
+    mm_dbg ("+CTZU URC received: %s", iso8601);
+    mm_iface_modem_time_update_network_time (MM_IFACE_MODEM_TIME (self), iso8601);
+    g_free (iso8601);
+
+    g_assert (tz);
+    mm_iface_modem_time_update_network_timezone (MM_IFACE_MODEM_TIME (self), tz);
+    g_object_unref (tz);
+}
+
+static void
+common_time_setup_cleanup_unsolicited_events (MMSharedCinterion *self,
+                                              gboolean           enable)
+{
+    Private        *priv;
+    MMPortSerialAt *ports[2];
+    guint           i;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    ports[0] = mm_base_modem_peek_port_primary   (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    mm_dbg ("%s up time unsolicited events...",
+            enable ? "Setting" : "Cleaning");
+
+    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
+        if (!ports[i])
+            continue;
+
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       priv->ctzu_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)ctzu_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+    }
+}
+
+/*****************************************************************************/
+/* Cleanup unsolicited events (Time interface) */
+
+gboolean
+mm_shared_cinterion_time_cleanup_unsolicited_events_finish (MMIfaceModemTime  *self,
+                                                            GAsyncResult      *res,
+                                                            GError           **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_time_cleanup_unsolicited_events_ready (MMIfaceModemTime *self,
+                                              GAsyncResult     *res,
+                                              GTask            *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_time_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent time unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_shared_cinterion_time_cleanup_unsolicited_events (MMIfaceModemTime    *self,
+                                                     GAsyncReadyCallback  callback,
+                                                     gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_time_parent);
+
+    /* our own cleanup first */
+    common_time_setup_cleanup_unsolicited_events (MM_SHARED_CINTERION (self), FALSE);
+
+    if (priv->iface_modem_time_parent->cleanup_unsolicited_events &&
+        priv->iface_modem_time_parent->cleanup_unsolicited_events_finish) {
+        /* Chain up parent's cleanup */
+        priv->iface_modem_time_parent->cleanup_unsolicited_events (
+            self,
+            (GAsyncReadyCallback)parent_time_cleanup_unsolicited_events_ready,
+            task);
+        return;
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+/*****************************************************************************/
+/* Setup unsolicited events (Time interface) */
+
+gboolean
+mm_shared_cinterion_time_setup_unsolicited_events_finish (MMIfaceModemTime  *self,
+                                                          GAsyncResult      *res,
+                                                          GError           **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+own_time_setup_unsolicited_events (GTask *task)
+{
+    MMSharedCinterion *self;
+
+    self = g_task_get_source_object (task);
+
+    /* our own setup next */
+    common_time_setup_cleanup_unsolicited_events (MM_SHARED_CINTERION (self), TRUE);
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+parent_time_setup_unsolicited_events_ready (MMIfaceModemTime *self,
+                                            GAsyncResult     *res,
+                                            GTask            *task)
+{
+    GError  *error = NULL;
+    Private *priv;
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+
+    if (!priv->iface_modem_time_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent time unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    own_time_setup_unsolicited_events (task);
+}
+
+void
+mm_shared_cinterion_time_setup_unsolicited_events (MMIfaceModemTime    *self,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data)
+{
+    Private *priv;
+    GTask   *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    priv = get_private (MM_SHARED_CINTERION (self));
+    g_assert (priv->iface_modem_time_parent);
+
+    if (priv->iface_modem_time_parent->setup_unsolicited_events &&
+        priv->iface_modem_time_parent->setup_unsolicited_events_finish) {
+        /* chain up parent's setup first */
+        priv->iface_modem_time_parent->setup_unsolicited_events (
+            self,
+            (GAsyncReadyCallback)parent_time_setup_unsolicited_events_ready,
+            task);
+        return;
+    }
+
+    own_time_setup_unsolicited_events (task);
+}
+
+/*****************************************************************************/
+
 static void
 shared_cinterion_init (gpointer g_iface)
 {
diff --git a/plugins/cinterion/mm-shared-cinterion.h b/plugins/cinterion/mm-shared-cinterion.h
index 310a538..6500dd4 100644
--- a/plugins/cinterion/mm-shared-cinterion.h
+++ b/plugins/cinterion/mm-shared-cinterion.h
@@ -12,6 +12,7 @@
  *
  * Copyright (C) 2014 Ammonit Measurement GmbH
  * Copyright (C) 2014 - 2018 Aleksander Morgado <aleksander@aleksander.es>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #ifndef MM_SHARED_CINTERION_H
@@ -26,6 +27,8 @@
 #include "mm-broadband-modem.h"
 #include "mm-iface-modem.h"
 #include "mm-iface-modem-location.h"
+#include "mm-iface-modem-voice.h"
+#include "mm-iface-modem-time.h"
 
 #define MM_TYPE_SHARED_CINTERION               (mm_shared_cinterion_get_type ())
 #define MM_SHARED_CINTERION(obj)               (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_SHARED_CINTERION, MMSharedCinterion))
@@ -39,10 +42,19 @@
 
     /* Peek location interface of the parent class of the object */
     MMIfaceModemLocation *  (* peek_parent_location_interface) (MMSharedCinterion *self);
+
+    /* Peek voice interface of the parent class of the object */
+    MMIfaceModemVoice *  (* peek_parent_voice_interface) (MMSharedCinterion *self);
+
+    /* Peek time interface of the parent class of the object */
+    MMIfaceModemTime *  (* peek_parent_time_interface) (MMSharedCinterion *self);
 };
 
 GType mm_shared_cinterion_get_type (void);
 
+/*****************************************************************************/
+/* Location interface */
+
 void                  mm_shared_cinterion_location_load_capabilities        (MMIfaceModemLocation *self,
                                                                              GAsyncReadyCallback callback,
                                                                              gpointer user_data);
@@ -66,4 +78,63 @@
                                                                              GAsyncResult *res,
                                                                              GError **error);
 
+/*****************************************************************************/
+/* Voice interface */
+
+MMBaseCall *mm_shared_cinterion_create_call (MMIfaceModemVoice *self,
+                                             MMCallDirection    direction,
+                                             const gchar       *number);
+
+void     mm_shared_cinterion_voice_check_support                     (MMIfaceModemVoice   *self,
+                                                                      GAsyncReadyCallback  callback,
+                                                                      gpointer             user_data);
+gboolean mm_shared_cinterion_voice_check_support_finish              (MMIfaceModemVoice    *self,
+                                                                      GAsyncResult         *res,
+                                                                      GError              **error);
+
+void     mm_shared_cinterion_voice_setup_unsolicited_events          (MMIfaceModemVoice    *self,
+                                                                      GAsyncReadyCallback   callback,
+                                                                      gpointer              user_data);
+gboolean mm_shared_cinterion_voice_setup_unsolicited_events_finish   (MMIfaceModemVoice    *self,
+                                                                      GAsyncResult         *res,
+                                                                      GError              **error);
+
+void     mm_shared_cinterion_voice_cleanup_unsolicited_events        (MMIfaceModemVoice    *self,
+                                                                      GAsyncReadyCallback   callback,
+                                                                      gpointer              user_data);
+gboolean mm_shared_cinterion_voice_cleanup_unsolicited_events_finish (MMIfaceModemVoice    *self,
+                                                                      GAsyncResult         *res,
+                                                                      GError              **error);
+
+void     mm_shared_cinterion_voice_enable_unsolicited_events         (MMIfaceModemVoice    *self,
+                                                                      GAsyncReadyCallback   callback,
+                                                                      gpointer              user_data);
+gboolean mm_shared_cinterion_voice_enable_unsolicited_events_finish  (MMIfaceModemVoice    *self,
+                                                                      GAsyncResult         *res,
+                                                                      GError              **error);
+
+void     mm_shared_cinterion_voice_disable_unsolicited_events        (MMIfaceModemVoice    *self,
+                                                                      GAsyncReadyCallback   callback,
+                                                                      gpointer              user_data);
+gboolean mm_shared_cinterion_voice_disable_unsolicited_events_finish (MMIfaceModemVoice    *self,
+                                                                      GAsyncResult         *res,
+                                                                      GError              **error);
+
+/*****************************************************************************/
+/* Time interface */
+
+void     mm_shared_cinterion_time_setup_unsolicited_events          (MMIfaceModemTime     *self,
+                                                                     GAsyncReadyCallback   callback,
+                                                                     gpointer              user_data);
+gboolean mm_shared_cinterion_time_setup_unsolicited_events_finish   (MMIfaceModemTime     *self,
+                                                                     GAsyncResult         *res,
+                                                                     GError              **error);
+
+void     mm_shared_cinterion_time_cleanup_unsolicited_events        (MMIfaceModemTime     *self,
+                                                                     GAsyncReadyCallback  callback,
+                                                                     gpointer             user_data);
+gboolean mm_shared_cinterion_time_cleanup_unsolicited_events_finish (MMIfaceModemTime     *self,
+                                                                     GAsyncResult         *res,
+                                                                     GError              **error);
+
 #endif  /* MM_SHARED_CINTERION_H */
diff --git a/plugins/cinterion/tests/test-modem-helpers-cinterion.c b/plugins/cinterion/tests/test-modem-helpers-cinterion.c
index 2578eb0..eef0a70 100644
--- a/plugins/cinterion/tests/test-modem-helpers-cinterion.c
+++ b/plugins/cinterion/tests/test-modem-helpers-cinterion.c
@@ -668,6 +668,195 @@
 }
 
 /*****************************************************************************/
+/* Test ^SLCC URCs */
+
+static void
+common_test_slcc_urc (const gchar               *urc,
+                      const MMCallInfo *expected_call_info_list,
+                      guint                      expected_call_info_list_size)
+{
+    GError     *error = NULL;
+    GRegex     *slcc_regex = NULL;
+    gboolean    result;
+    GMatchInfo *match_info = NULL;
+    gchar      *str;
+    GList      *call_info_list = NULL;
+    GList      *l;
+
+
+    slcc_regex = mm_cinterion_get_slcc_regex ();
+
+    /* Same matching logic as done in MMSerialPortAt when processing URCs! */
+    result = g_regex_match_full (slcc_regex, urc, -1, 0, 0, &match_info, &error);
+    g_assert_no_error (error);
+    g_assert (result);
+
+    /* read full matched content */
+    str = g_match_info_fetch (match_info, 0);
+    g_assert (str);
+
+    result = mm_cinterion_parse_slcc_list (str, &call_info_list, &error);
+    g_assert_no_error (error);
+    g_assert (result);
+
+    g_print ("found %u calls\n", g_list_length (call_info_list));
+
+    if (expected_call_info_list) {
+        g_assert (call_info_list);
+        g_assert_cmpuint (g_list_length (call_info_list), ==, expected_call_info_list_size);
+    } else
+        g_assert (!call_info_list);
+
+    for (l = call_info_list; l; l = g_list_next (l)) {
+        const MMCallInfo *call_info = (const MMCallInfo *)(l->data);
+        gboolean                   found = FALSE;
+        guint                      i;
+
+        g_print ("call at index %u: direction %s, state %s, number %s\n",
+                 call_info->index,
+                 mm_call_direction_get_string (call_info->direction),
+                 mm_call_state_get_string (call_info->state),
+                 call_info->number ? call_info->number : "n/a");
+
+        for (i = 0; !found && i < expected_call_info_list_size; i++)
+            found = ((call_info->index == expected_call_info_list[i].index) &&
+                     (call_info->direction  == expected_call_info_list[i].direction) &&
+                     (call_info->state  == expected_call_info_list[i].state) &&
+                     (g_strcmp0 (call_info->number, expected_call_info_list[i].number) == 0));
+
+        g_assert (found);
+    }
+
+    g_match_info_free (match_info);
+    g_regex_unref (slcc_regex);
+    g_free (str);
+
+    mm_cinterion_call_info_list_free (call_info_list);
+}
+
+static void
+test_slcc_urc_empty (void)
+{
+    const gchar *urc = "\r\n^SLCC: \r\n";
+
+    common_test_slcc_urc (urc, NULL, 0);
+}
+
+static void
+test_slcc_urc_single (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE, "123456789" }
+    };
+
+    const gchar *urc =
+        "\r\n^SLCC: 1,1,0,0,0,0,\"123456789\",161"
+        "\r\n^SLCC: \r\n";
+
+    common_test_slcc_urc (urc, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+static void
+test_slcc_urc_multiple (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  NULL        },
+        { 2, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "123456789" },
+        { 3, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "987654321" },
+    };
+
+    const gchar *urc =
+        "\r\n^SLCC: 1,1,0,0,1,0" /* number unknown */
+        "\r\n^SLCC: 2,1,0,0,1,0,\"123456789\",161"
+        "\r\n^SLCC: 3,1,0,0,1,0,\"987654321\",161,\"Alice\""
+        "\r\n^SLCC: \r\n";
+
+    common_test_slcc_urc (urc, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+static void
+test_slcc_urc_complex (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "123456789" },
+        { 2, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_WAITING, "987654321" },
+    };
+
+    const gchar *urc =
+        "\r\n^CIEV: 1,0" /* some different URC before our match */
+        "\r\n^SLCC: 1,1,0,0,0,0,\"123456789\",161"
+        "\r\n^SLCC: 2,1,5,0,0,0,\"987654321\",161"
+        "\r\n^SLCC: \r\n"
+        "\r\n^CIEV: 1,0" /* some different URC after our match */;
+
+    common_test_slcc_urc (urc, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+/*****************************************************************************/
+/* Test +CTZU URCs */
+
+static void
+common_test_ctzu_urc (const gchar *urc,
+                      const gchar *expected_iso8601,
+                      gint         expected_offset,
+                      gint         expected_dst_offset)
+{
+    GError            *error = NULL;
+    GRegex            *ctzu_regex = NULL;
+    gboolean           result;
+    GMatchInfo        *match_info = NULL;
+    gchar             *iso8601;
+    MMNetworkTimezone *tz = NULL;
+
+    ctzu_regex = mm_cinterion_get_ctzu_regex ();
+
+    /* Same matching logic as done in MMSerialPortAt when processing URCs! */
+    result = g_regex_match_full (ctzu_regex, urc, -1, 0, 0, &match_info, &error);
+    g_assert_no_error (error);
+    g_assert (result);
+
+    result = mm_cinterion_parse_ctzu_urc (match_info, &iso8601, &tz, &error);
+    g_assert_no_error (error);
+    g_assert (result);
+
+    g_assert (iso8601);
+    g_assert_cmpstr (expected_iso8601, ==, iso8601);
+    g_free (iso8601);
+
+    g_assert (tz);
+    g_assert_cmpint (expected_offset, ==, mm_network_timezone_get_offset (tz));
+
+    if (expected_dst_offset >= 0)
+        g_assert_cmpuint ((guint)expected_dst_offset, ==, mm_network_timezone_get_dst_offset (tz));
+
+    g_object_unref (tz);
+    g_match_info_free (match_info);
+    g_regex_unref (ctzu_regex);
+}
+
+static void
+test_ctzu_urc_simple (void)
+{
+    const gchar *urc = "\r\n+CTZU: \"19/07/09,11:15:40\",+08\r\n";
+    const gchar *expected_iso8601    = "2019-07-09T11:15:40+02:00";
+    gint         expected_offset     = 120;
+    gint         expected_dst_offset = -1; /* not given */
+
+    common_test_ctzu_urc (urc, expected_iso8601, expected_offset, expected_dst_offset);
+}
+
+static void
+test_ctzu_urc_full (void)
+{
+    const gchar *urc = "\r\n+CTZU: \"19/07/09,11:15:40\",+08,1\r\n";
+    const gchar *expected_iso8601    = "2019-07-09T11:15:40+02:00";
+    gint         expected_offset     = 120;
+    gint         expected_dst_offset = 60;
+
+    common_test_ctzu_urc (urc, expected_iso8601, expected_offset, expected_dst_offset);
+}
+
+/*****************************************************************************/
 
 void
 _mm_log (const char *loc,
@@ -706,6 +895,12 @@
     g_test_add_func ("/MM/cinterion/sind/response/simstatus", test_sind_response_simstatus);
     g_test_add_func ("/MM/cinterion/smong/response/tc63i",    test_smong_response_tc63i);
     g_test_add_func ("/MM/cinterion/smong/response/other",    test_smong_response_other);
+    g_test_add_func ("/MM/cinterion/slcc/urc/empty",          test_slcc_urc_empty);
+    g_test_add_func ("/MM/cinterion/slcc/urc/single",         test_slcc_urc_single);
+    g_test_add_func ("/MM/cinterion/slcc/urc/multiple",       test_slcc_urc_multiple);
+    g_test_add_func ("/MM/cinterion/slcc/urc/complex",        test_slcc_urc_complex);
+    g_test_add_func ("/MM/cinterion/ctzu/urc/simple",         test_ctzu_urc_simple);
+    g_test_add_func ("/MM/cinterion/ctzu/urc/full",           test_ctzu_urc_full);
 
     return g_test_run ();
 }
diff --git a/plugins/dell/77-mm-dell-port-types.rules b/plugins/dell/77-mm-dell-port-types.rules
index c092261..8efc2fc 100644
--- a/plugins/dell/77-mm-dell-port-types.rules
+++ b/plugins/dell/77-mm-dell-port-types.rules
@@ -18,5 +18,11 @@
 ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_TYPE_GPS}="1"
 ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_PORT_TYPE_QCDM}="1"
 
+# Dell DW5820e
+#  if 02: AT port
+#  if 04: debug port (ignore)
+#  if 06: AT port
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d9", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_IGNORE}="1"
+
 GOTO="mm_dell_port_types_end"
 LABEL="mm_dell_port_types_end"
diff --git a/plugins/dlink/77-mm-dlink-port-types.rules b/plugins/dlink/77-mm-dlink-port-types.rules
new file mode 100644
index 0000000..6489b7c
--- /dev/null
+++ b/plugins/dlink/77-mm-dlink-port-types.rules
@@ -0,0 +1,16 @@
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move|bind", GOTO="mm_dlink_port_types_end"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="2001", GOTO="mm_dlink_port_types"
+GOTO="mm_dlink_port_types_end"
+
+LABEL="mm_dlink_port_types"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# D-Link DWM-222
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7e35", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7e35", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7e35", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7e35", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_PORT_IGNORE}="1"
+
+LABEL="mm_dlink_port_types_end"
diff --git a/plugins/dlink/mm-plugin-dlink.c b/plugins/dlink/mm-plugin-dlink.c
new file mode 100644
index 0000000..e7c6816
--- /dev/null
+++ b/plugins/dlink/mm-plugin-dlink.c
@@ -0,0 +1,94 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <string.h>
+#include <gmodule.h>
+
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+#include "mm-port-enums-types.h"
+#include "mm-log.h"
+#include "mm-plugin-dlink.h"
+#include "mm-broadband-modem.h"
+
+#if defined WITH_QMI
+# include "mm-broadband-modem-qmi.h"
+#endif
+
+G_DEFINE_TYPE (MMPluginDlink, mm_plugin_dlink, MM_TYPE_PLUGIN)
+
+MM_PLUGIN_DEFINE_MAJOR_VERSION
+MM_PLUGIN_DEFINE_MINOR_VERSION
+
+/*****************************************************************************/
+
+static MMBaseModem *
+create_modem (MMPlugin     *self,
+              const gchar  *uid,
+              const gchar **drivers,
+              guint16       vendor,
+              guint16       product,
+              GList        *probes,
+              GError      **error)
+{
+#if defined WITH_QMI
+    if (mm_port_probe_list_has_qmi_port (probes)) {
+        mm_dbg ("QMI-powered D-Link modem found...");
+        return MM_BASE_MODEM (mm_broadband_modem_qmi_new (uid,
+                                                          drivers,
+                                                          mm_plugin_get_name (self),
+                                                          vendor,
+                                                          product));
+    }
+#endif
+
+    return MM_BASE_MODEM (mm_broadband_modem_new (uid,
+                                                  drivers,
+                                                  mm_plugin_get_name (self),
+                                                  vendor,
+                                                  product));
+}
+
+/*****************************************************************************/
+
+G_MODULE_EXPORT MMPlugin *
+mm_plugin_create (void)
+{
+    static const gchar *subsystems[] = { "tty", "net", "usb", NULL };
+    static const guint16 vendor_ids[] = { 0x2001, 0 };
+
+    return MM_PLUGIN (
+        g_object_new (MM_TYPE_PLUGIN_DLINK,
+                      MM_PLUGIN_NAME,               "D-Link",
+                      MM_PLUGIN_ALLOWED_SUBSYSTEMS, subsystems,
+                      MM_PLUGIN_ALLOWED_VENDOR_IDS, vendor_ids,
+                      MM_PLUGIN_ALLOWED_AT,         TRUE,
+                      MM_PLUGIN_ALLOWED_QMI,        TRUE,
+                      NULL));
+}
+
+static void
+mm_plugin_dlink_init (MMPluginDlink *self)
+{
+}
+
+static void
+mm_plugin_dlink_class_init (MMPluginDlinkClass *klass)
+{
+    MMPluginClass *plugin_class = MM_PLUGIN_CLASS (klass);
+
+    plugin_class->create_modem = create_modem;
+}
diff --git a/plugins/dlink/mm-plugin-dlink.h b/plugins/dlink/mm-plugin-dlink.h
new file mode 100644
index 0000000..13453cb
--- /dev/null
+++ b/plugins/dlink/mm-plugin-dlink.h
@@ -0,0 +1,40 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef MM_PLUGIN_DLINK_H
+#define MM_PLUGIN_DLINK_H
+
+#include "mm-plugin.h"
+
+#define MM_TYPE_PLUGIN_DLINK            (mm_plugin_dlink_get_type ())
+#define MM_PLUGIN_DLINK(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_PLUGIN_DLINK, MMPluginDlink))
+#define MM_PLUGIN_DLINK_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_PLUGIN_DLINK, MMPluginDlinkClass))
+#define MM_IS_PLUGIN_DLINK(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_PLUGIN_DLINK))
+#define MM_IS_PLUGIN_DLINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_PLUGIN_DLINK))
+#define MM_PLUGIN_DLINK_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_PLUGIN_DLINK, MMPluginDlinkClass))
+
+typedef struct {
+    MMPlugin parent;
+} MMPluginDlink;
+
+typedef struct {
+    MMPluginClass parent;
+} MMPluginDlinkClass;
+
+GType mm_plugin_dlink_get_type (void);
+
+G_MODULE_EXPORT MMPlugin *mm_plugin_create (void);
+
+#endif /* MM_PLUGIN_DLINK_H */
diff --git a/plugins/generic/tests/test-service-generic.c b/plugins/generic/tests/test-service-generic.c
index c6e4277..ea29e9e 100644
--- a/plugins/generic/tests/test-service-generic.c
+++ b/plugins/generic/tests/test-service-generic.c
@@ -37,7 +37,7 @@
 
     /* Create port name, and add process ID so that multiple runs of this test
      * in the same system don't clash with each other */
-	ports[0] = g_strdup_printf ("abstract:port0:%ld", (glong) getpid ());
+    ports[0] = g_strdup_printf ("abstract:port0:%ld", (glong) getpid ());
     g_debug ("test service generic: using abstract port at '%s'", ports[0]);
 
     /* Setup new port context */
diff --git a/plugins/huawei/mm-broadband-modem-huawei.c b/plugins/huawei/mm-broadband-modem-huawei.c
index d4c0e83..76490a9 100644
--- a/plugins/huawei/mm-broadband-modem-huawei.c
+++ b/plugins/huawei/mm-broadband-modem-huawei.c
@@ -14,8 +14,8 @@
  * Copyright (C) 2009 - 2012 Red Hat, Inc.
  * Copyright (C) 2011 - 2012 Google Inc.
  * Copyright (C) 2012 Huawei Technologies Co., Ltd
- * Copyright (C) 2012 - 2013 Aleksander Morgado <aleksander@gnu.org>
  * Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2012 - 2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include <config.h>
@@ -49,7 +49,6 @@
 #include "mm-broadband-bearer.h"
 #include "mm-bearer-list.h"
 #include "mm-sim-huawei.h"
-#include "mm-call-huawei.h"
 
 static void iface_modem_init (MMIfaceModem *iface);
 static void iface_modem_3gpp_init (MMIfaceModem3gpp *iface);
@@ -103,6 +102,13 @@
     GRegex *dsflowrpt_regex;
     GRegex *ndisstat_regex;
 
+    /* Regex for voice management notifications */
+    GRegex *orig_regex;
+    GRegex *conf_regex;
+    GRegex *conn_regex;
+    GRegex *cend_regex;
+    GRegex *ddtmf_regex;
+
     /* Regex to ignore */
     GRegex *boot_regex;
     GRegex *connect_regex;
@@ -123,7 +129,6 @@
     GRegex *ltersrp_regex;
     GRegex *cschannelinfo_regex;
     GRegex *eons_regex;
-    GRegex *orig_regex;
 
     FeatureSupport ndisdup_support;
     FeatureSupport rfswitch_support;
@@ -2934,20 +2939,363 @@
 }
 
 /*****************************************************************************/
-/* Enabling unsolicited events (Voice interface) */
+/* In-call audio channel setup/cleanup */
 
 static gboolean
-modem_voice_enable_unsolicited_events_finish (MMIfaceModemVoice *self,
-                                              GAsyncResult *res,
-                                              GError **error)
+modem_voice_setup_in_call_audio_channel_finish (MMIfaceModemVoice  *_self,
+                                                GAsyncResult       *res,
+                                                MMPort            **audio_port,
+                                                MMCallAudioFormat **audio_format,
+                                                GError            **error)
+{
+    MMBroadbandModemHuawei *self = MM_BROADBAND_MODEM_HUAWEI (_self);
+
+    if (!g_task_propagate_boolean (G_TASK (res), error))
+        return FALSE;
+
+    if (self->priv->cvoice_support == FEATURE_SUPPORTED) {
+        /* Setup audio format */
+        if (audio_format) {
+            gchar *resolution_str;
+
+            resolution_str = g_strdup_printf ("s%ule", self->priv->audio_bits);
+            *audio_format = mm_call_audio_format_new ();
+            mm_call_audio_format_set_encoding   (*audio_format, "pcm");
+            mm_call_audio_format_set_resolution (*audio_format, resolution_str);
+            mm_call_audio_format_set_rate       (*audio_format, self->priv->audio_hz);
+            g_free (resolution_str);
+        }
+
+        /* The QCDM port, if present, switches from QCDM to voice while
+         * a voice call is active. */
+        if (audio_port)
+            *audio_port = MM_PORT (mm_base_modem_get_port_qcdm (MM_BASE_MODEM (self)));
+    } else {
+        if (audio_format)
+            *audio_format =  NULL;
+        if (audio_port)
+            *audio_port =  NULL;
+    }
+
+    return TRUE;
+}
+
+static void
+ddsetex_ready (MMBaseModem  *self,
+               GAsyncResult *res,
+               GTask        *task)
+{
+    GError *error = NULL;
+
+    if (!mm_base_modem_at_command_finish (self, res, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_setup_in_call_audio_channel (MMIfaceModemVoice   *_self,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+    MMBroadbandModemHuawei *self = MM_BROADBAND_MODEM_HUAWEI (_self);
+    GTask                  *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* If there is no CVOICE support, no custom audio setup required
+     * (i.e. audio path is externally managed) */
+    if (self->priv->cvoice_support != FEATURE_SUPPORTED) {
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Enable audio streaming on the audio port */
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "^DDSETEX=2",
+                              5,
+                              FALSE,
+                              (GAsyncReadyCallback)ddsetex_ready,
+                              task);
+}
+
+/*****************************************************************************/
+/* Common setup/cleanup voice unsolicited events */
+
+typedef enum {
+    HUAWEI_CALL_TYPE_VOICE                  = 0,
+    HUAWEI_CALL_TYPE_CS_DATA                = 1,
+    HUAWEI_CALL_TYPE_PS_DATA                = 2,
+    HUAWEI_CALL_TYPE_CDMA_SMS               = 3,
+    HUAWEI_CALL_TYPE_OTA_STANDARD_OTASP     = 7,
+    HUAWEI_CALL_TYPE_OTA_NON_STANDARD_OTASP = 8,
+    HUAWEI_CALL_TYPE_EMERGENCY              = 9,
+} HuaweiCallType;
+
+static void
+orig_received (MMPortSerialAt         *port,
+               GMatchInfo             *match_info,
+               MMBroadbandModemHuawei *self)
+{
+    MMCallInfo call_info = { 0 };
+    guint      aux       = 0;
+
+    if (!mm_get_uint_from_match_info (match_info, 2, &aux)) {
+        mm_warn ("couldn't parse call type from ^ORIG");
+        return;
+    }
+    if (aux != HUAWEI_CALL_TYPE_VOICE && aux != HUAWEI_CALL_TYPE_EMERGENCY) {
+        mm_dbg ("ignored ^ORIG for non-voice call");
+        return;
+    }
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &aux)) {
+        mm_warn ("couldn't parse call index from ^ORIG");
+        return;
+    }
+    call_info.index     = aux;
+    call_info.state     = MM_CALL_STATE_DIALING;
+    call_info.direction = MM_CALL_DIRECTION_OUTGOING;
+
+    mm_dbg ("call %u state updated: dialing", call_info.index);
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+conf_received (MMPortSerialAt         *port,
+               GMatchInfo             *match_info,
+               MMBroadbandModemHuawei *self)
+{
+    MMCallInfo call_info = { 0 };
+    guint      aux       = 0;
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &aux)) {
+        mm_warn ("couldn't parse call index from ^CONF");
+        return;
+    }
+    call_info.index     = aux;
+    call_info.state     = MM_CALL_STATE_RINGING_OUT;
+    call_info.direction = MM_CALL_DIRECTION_OUTGOING;
+
+    mm_dbg ("call %u state updated: ringing-out", call_info.index);
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+conn_received (MMPortSerialAt         *port,
+               GMatchInfo             *match_info,
+               MMBroadbandModemHuawei *self)
+{
+    MMCallInfo call_info = { 0 };
+    guint      aux       = 0;
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &aux)) {
+        mm_warn ("couldn't parse call index from ^CONN");
+        return;
+    }
+    call_info.index     = aux;
+    call_info.state     = MM_CALL_STATE_ACTIVE;
+    call_info.direction = MM_CALL_DIRECTION_UNKNOWN;
+
+    mm_dbg ("call %u state updated: active", aux);
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+cend_received (MMPortSerialAt         *port,
+               GMatchInfo             *match_info,
+               MMBroadbandModemHuawei *self)
+{
+    MMCallInfo call_info  = { 0 };
+    guint      aux        = 0;
+
+    /* only index is mandatory */
+    if (!mm_get_uint_from_match_info (match_info, 1, &aux)) {
+        mm_warn ("couldn't parse call index from ^CEND");
+        return;
+    }
+    call_info.index = aux;
+    call_info.state = MM_CALL_STATE_TERMINATED;
+    call_info.direction = MM_CALL_DIRECTION_UNKNOWN;
+
+    mm_dbg ("call %u state updated: terminated", call_info.index);
+    if (mm_get_uint_from_match_info (match_info, 2, &aux))
+        mm_dbg ("  call duration: %u seconds", aux);
+    if (mm_get_uint_from_match_info (match_info, 3, &aux))
+        mm_dbg ("  end status code: %u", aux);
+    if (mm_get_uint_from_match_info (match_info, 4, &aux))
+        mm_dbg ("  call control cause: %u", aux);
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+ddtmf_received (MMPortSerialAt         *port,
+                GMatchInfo             *match_info,
+                MMBroadbandModemHuawei *self)
+{
+    gchar *dtmf;
+
+    dtmf = g_match_info_fetch (match_info, 1);
+    mm_dbg ("received DTMF: %s", dtmf);
+    /* call index unknown */
+    mm_iface_modem_voice_received_dtmf (MM_IFACE_MODEM_VOICE (self), 0, dtmf);
+    g_free (dtmf);
+}
+
+static void
+common_voice_setup_cleanup_unsolicited_events (MMBroadbandModemHuawei *self,
+                                               gboolean                enable)
+{
+    MMPortSerialAt *ports[2];
+    guint           i;
+
+    ports[0] = mm_base_modem_peek_port_primary   (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
+        if (!ports[i])
+            continue;
+
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->orig_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)orig_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->conf_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)conf_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->conn_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)conn_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->cend_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)cend_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->ddtmf_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)ddtmf_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+    }
+}
+
+/*****************************************************************************/
+/* Setup unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_setup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                             GAsyncResult       *res,
+                                             GError            **error)
 {
     return g_task_propagate_boolean (G_TASK (res), error);
 }
 
 static void
-own_voice_enable_unsolicited_events_ready (MMBaseModem *self,
+parent_voice_setup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                              GAsyncResult      *res,
+                                              GTask             *task)
+{
+    GError *error = NULL;
+
+    if (!iface_modem_voice_parent->setup_unsolicited_events_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Our own setup now */
+    common_voice_setup_cleanup_unsolicited_events (MM_BROADBAND_MODEM_HUAWEI (self), TRUE);
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_setup_unsolicited_events (MMIfaceModemVoice   *self,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* Chain up parent's setup */
+    iface_modem_voice_parent->setup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_setup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Cleanup unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_cleanup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                               GAsyncResult       *res,
+                                               GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_cleanup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
+{
+    GError *error = NULL;
+
+    if (!iface_modem_voice_parent->cleanup_unsolicited_events_finish (self, res, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_cleanup_unsolicited_events (MMIfaceModemVoice   *self,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* cleanup our own */
+    common_voice_setup_cleanup_unsolicited_events (MM_BROADBAND_MODEM_HUAWEI (self), FALSE);
+
+    /* Chain up parent's cleanup */
+    iface_modem_voice_parent->cleanup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_cleanup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Enabling unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_enable_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                              GAsyncResult       *res,
+                                              GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+own_voice_enable_unsolicited_events_ready (MMBaseModem  *self,
                                            GAsyncResult *res,
-                                           GTask *task)
+                                           GTask        *task)
 {
     GError *error = NULL;
 
@@ -2967,8 +3315,8 @@
 
 static void
 parent_voice_enable_unsolicited_events_ready (MMIfaceModemVoice *self,
-                                              GAsyncResult *res,
-                                              GTask *task)
+                                              GAsyncResult      *res,
+                                              GTask             *task)
 {
     GError *error = NULL;
 
@@ -2991,9 +3339,9 @@
 }
 
 static void
-modem_voice_enable_unsolicited_events (MMIfaceModemVoice *self,
-                                       GAsyncReadyCallback callback,
-                                       gpointer user_data)
+modem_voice_enable_unsolicited_events (MMIfaceModemVoice   *self,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
 {
     GTask *task;
 
@@ -3010,17 +3358,17 @@
 /* Disabling unsolicited events (Voice interface) */
 
 static gboolean
-modem_voice_disable_unsolicited_events_finish (MMIfaceModemVoice *self,
-                                               GAsyncResult *res,
-                                               GError **error)
+modem_voice_disable_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                               GAsyncResult       *res,
+                                               GError            **error)
 {
     return g_task_propagate_boolean (G_TASK (res), error);
 }
 
 static void
-own_voice_disable_unsolicited_events_ready (MMBaseModem *self,
+own_voice_disable_unsolicited_events_ready (MMBaseModem  *self,
                                             GAsyncResult *res,
-                                            GTask *task)
+                                            GTask        *task)
 {
     GError *error = NULL;
 
@@ -3039,15 +3387,19 @@
 };
 
 static void
-modem_voice_disable_unsolicited_events (MMIfaceModemVoice *self,
-                                        GAsyncReadyCallback callback,
-                                        gpointer user_data)
+parent_voice_disable_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
 {
-    GTask *task;
+    GError *error = NULL;
 
-    task = g_task_new (self, NULL, callback, user_data);
+    if (!iface_modem_voice_parent->disable_unsolicited_events_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
 
-    /* No unsolicited events disabling in parent */
+    /* our own disable now */
 
     mm_base_modem_at_sequence_full (
         MM_BASE_MODEM (self),
@@ -3060,25 +3412,36 @@
         task);
 }
 
+static void
+modem_voice_disable_unsolicited_events (MMIfaceModemVoice   *self,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* Chain up parent's disable */
+    iface_modem_voice_parent->disable_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_disable_unsolicited_events_ready,
+        task);
+}
+
 /*****************************************************************************/
 /* Create call (Voice interface) */
 
 static MMBaseCall *
-create_call (MMIfaceModemVoice *_self,
+create_call (MMIfaceModemVoice *self,
              MMCallDirection    direction,
              const gchar       *number)
 {
-    MMBroadbandModemHuawei *self = MM_BROADBAND_MODEM_HUAWEI (_self);
-
-    /* If CVOICE is supported we must have audio settings */
-    g_assert (self->priv->cvoice_support == FEATURE_NOT_SUPPORTED ||
-              (self->priv->cvoice_support == FEATURE_SUPPORTED && self->priv->audio_hz && self->priv->audio_bits));
-
-    return mm_call_huawei_new (MM_BASE_MODEM (self),
-                               direction,
-                               number,
-                               self->priv->audio_hz,
-                               self->priv->audio_bits);
+    return mm_base_call_new (MM_BASE_MODEM (self),
+                             direction,
+                             number,
+                             TRUE,  /* skip_incoming_timeout */
+                             TRUE,  /* supports_dialing_to_ringing */
+                             TRUE); /* supports_ringing_to_active) */
 }
 
 /*****************************************************************************/
@@ -4019,10 +4382,6 @@
             port,
             self->priv->eons_regex,
             NULL, NULL, NULL);
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->orig_regex,
-            NULL, NULL, NULL);
     }
 
     g_list_free_full (ports, g_object_unref);
@@ -4108,6 +4467,18 @@
                                                G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
     self->priv->ndisstat_regex = g_regex_new ("\\r\\n(\\^NDISSTAT:.+)\\r+\\n",
                                               G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+
+    self->priv->orig_regex = g_regex_new ("\\r\\n\\^ORIG:\\s*(\\d+),\\s*(\\d+)\\r\\n",
+                                          G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    self->priv->conf_regex = g_regex_new ("\\r\\n\\^CONF:\\s*(\\d+)\\r\\n",
+                                          G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    self->priv->conn_regex = g_regex_new ("\\r\\n\\^CONN:\\s*(\\d+),\\s*(\\d+)\\r\\n",
+                                          G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    self->priv->cend_regex = g_regex_new ("\\r\\n\\^CEND:\\s*(\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*(\\d*))?\\r\\n",
+                                          G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    self->priv->ddtmf_regex = g_regex_new ("\\r\\n\\^DDTMF:\\s*([0-9A-D\\*\\#])\\r\\n",
+                                           G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+
     self->priv->boot_regex = g_regex_new ("\\r\\n\\^BOOT:.+\\r\\n",
                                           G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
     self->priv->connect_regex = g_regex_new ("\\r\\n\\^CONNECT .+\\r\\n",
@@ -4146,8 +4517,6 @@
                                                     G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
     self->priv->eons_regex = g_regex_new ("\\r\\n\\^EONS:.+\\r\\n",
                                           G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-    self->priv->orig_regex = g_regex_new ("\\r\\n\\^ORIG:.+\\r\\n",
-                                              G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
 
     self->priv->ndisdup_support = FEATURE_SUPPORT_UNKNOWN;
     self->priv->rfswitch_support = FEATURE_SUPPORT_UNKNOWN;
@@ -4181,6 +4550,12 @@
     g_regex_unref (self->priv->mode_regex);
     g_regex_unref (self->priv->dsflowrpt_regex);
     g_regex_unref (self->priv->ndisstat_regex);
+    g_regex_unref (self->priv->orig_regex);
+    g_regex_unref (self->priv->conf_regex);
+    g_regex_unref (self->priv->conn_regex);
+    g_regex_unref (self->priv->cend_regex);
+    g_regex_unref (self->priv->ddtmf_regex);
+
     g_regex_unref (self->priv->boot_regex);
     g_regex_unref (self->priv->connect_regex);
     g_regex_unref (self->priv->csnr_regex);
@@ -4200,7 +4575,6 @@
     g_regex_unref (self->priv->ltersrp_regex);
     g_regex_unref (self->priv->cschannelinfo_regex);
     g_regex_unref (self->priv->eons_regex);
-    g_regex_unref (self->priv->orig_regex);
 
     if (self->priv->syscfg_supported_modes)
         g_array_unref (self->priv->syscfg_supported_modes);
@@ -4317,10 +4691,16 @@
 
     iface->check_support = modem_voice_check_support;
     iface->check_support_finish = modem_voice_check_support_finish;
+    iface->setup_unsolicited_events = modem_voice_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish = modem_voice_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events = modem_voice_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = modem_voice_cleanup_unsolicited_events_finish;
     iface->enable_unsolicited_events = modem_voice_enable_unsolicited_events;
     iface->enable_unsolicited_events_finish = modem_voice_enable_unsolicited_events_finish;
     iface->disable_unsolicited_events = modem_voice_disable_unsolicited_events;
     iface->disable_unsolicited_events_finish = modem_voice_disable_unsolicited_events_finish;
+    iface->setup_in_call_audio_channel = modem_voice_setup_in_call_audio_channel;
+    iface->setup_in_call_audio_channel_finish = modem_voice_setup_in_call_audio_channel_finish;
 
     iface->create_call = create_call;
 }
diff --git a/plugins/huawei/mm-call-huawei.c b/plugins/huawei/mm-call-huawei.c
deleted file mode 100644
index 6c7a885..0000000
--- a/plugins/huawei/mm-call-huawei.c
+++ /dev/null
@@ -1,445 +0,0 @@
-/* -*- 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) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
- */
-
-#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-log.h"
-#include "mm-base-modem-at.h"
-#include "mm-broadband-modem-huawei.h"
-#include "mm-call-huawei.h"
-
-G_DEFINE_TYPE (MMCallHuawei, mm_call_huawei, MM_TYPE_BASE_CALL)
-
-enum {
-    PROP_0,
-    PROP_AUDIO_HZ,
-    PROP_AUDIO_BITS,
-    PROP_LAST
-};
-
-static GParamSpec *properties[PROP_LAST];
-
-struct _MMCallHuaweiPrivate {
-    GRegex *conf_regex;
-    GRegex *conn_regex;
-    GRegex *cend_regex;
-    GRegex *ddtmf_regex;
-    guint   audio_hz;
-    guint   audio_bits;
-};
-
-/*****************************************************************************/
-/* Audio channel setup */
-
-typedef struct {
-    MMBaseModem       *modem;
-    MMPort            *audio_port;
-    MMCallAudioFormat *audio_format;
-} SetupAudioChannelContext;
-
-static void
-setup_audio_channel_context_free (SetupAudioChannelContext *ctx)
-{
-    g_clear_object (&ctx->audio_port);
-    g_clear_object (&ctx->audio_format);
-    g_clear_object (&ctx->modem);
-    g_slice_free (SetupAudioChannelContext, ctx);
-}
-
-static gboolean
-setup_audio_channel_finish (MMBaseCall         *self,
-                            GAsyncResult       *res,
-                            MMPort            **audio_port,
-                            MMCallAudioFormat **audio_format,
-                            GError            **error)
-{
-    SetupAudioChannelContext *ctx;
-
-    if (!g_task_propagate_boolean (G_TASK (res), error))
-        return FALSE;
-
-    ctx = g_task_get_task_data (G_TASK (res));
-
-    if (audio_port && ctx->audio_port)
-        *audio_port = g_object_ref (ctx->audio_port);
-    if (audio_format && ctx->audio_format)
-        *audio_format = g_object_ref (ctx->audio_format);
-
-    return TRUE;
-}
-
-static void
-ddsetex_ready (MMBaseModem  *modem,
-               GAsyncResult *res,
-               GTask        *task)
-{
-    MMCallHuawei             *self;
-    SetupAudioChannelContext *ctx;
-    GError                   *error = NULL;
-    const gchar              *response = NULL;
-    gchar                    *resolution_str;
-
-    response = mm_base_modem_at_command_finish (modem, res, &error);
-    if (!response) {
-        mm_dbg ("Error enabling audio streaming: '%s'", error->message);
-        g_task_return_error (task, error);
-        g_object_unref (task);
-        return;
-    }
-
-    self = g_task_get_source_object (task);
-    ctx  = g_task_get_task_data (task);
-
-    /* Setup audio format */
-    g_assert (self->priv->audio_hz && self->priv->audio_bits);
-    resolution_str = g_strdup_printf ("s%ule", self->priv->audio_bits);
-    ctx->audio_format = mm_call_audio_format_new ();
-    mm_call_audio_format_set_encoding   (ctx->audio_format, "pcm");
-    mm_call_audio_format_set_resolution (ctx->audio_format, resolution_str);
-    mm_call_audio_format_set_rate       (ctx->audio_format, self->priv->audio_hz);
-
-    /* The QCDM port, if present, switches from QCDM to voice while
-     * a voice call is active. */
-    ctx->audio_port = MM_PORT (mm_base_modem_get_port_qcdm (modem));
-
-    g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
-}
-
-static void
-setup_audio_channel (MMBaseCall           *_self,
-                     GAsyncReadyCallback   callback,
-                     gpointer              user_data)
-{
-    SetupAudioChannelContext *ctx;
-    MMCallHuawei             *self;
-    GTask                    *task;
-    MMBaseModem              *modem = NULL;
-
-    self = MM_CALL_HUAWEI (_self);
-
-    task = g_task_new (self, NULL, callback, user_data);
-
-    /* If there is no CVOICE support, no custom audio setup required
-     * (i.e. audio path is externally managed) */
-    if (!self->priv->audio_hz && !self->priv->audio_bits) {
-        g_task_return_boolean (task, TRUE);
-        g_object_unref (task);
-        return;
-    }
-
-    ctx = g_slice_new0 (SetupAudioChannelContext);
-    g_object_get (self,
-                  MM_BASE_CALL_MODEM, &ctx->modem,
-                  NULL);
-    g_task_set_task_data (task, ctx, (GDestroyNotify) setup_audio_channel_context_free);
-
-    /* Enable audio streaming on the audio port */
-    mm_base_modem_at_command (modem,
-                              "AT^DDSETEX=2",
-                              5,
-                              FALSE,
-                              (GAsyncReadyCallback)ddsetex_ready,
-                              task);
-}
-
-/*****************************************************************************/
-/* In-call unsolicited events */
-
-static void
-huawei_voice_ringback_tone (MMPortSerialAt *port,
-                            GMatchInfo     *match_info,
-                            MMCallHuawei   *self)
-{
-    guint call_x = 0;
-
-    if (!mm_get_uint_from_match_info (match_info, 1, &call_x))
-        return;
-
-    mm_dbg ("Ringback tone from call id '%u'", call_x);
-
-    if (mm_gdbus_call_get_state (MM_GDBUS_CALL (self)) == MM_CALL_STATE_DIALING)
-        mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_RINGING_OUT, MM_CALL_STATE_REASON_OUTGOING_STARTED);
-}
-
-static void
-huawei_voice_call_connection (MMPortSerialAt *port,
-                              GMatchInfo     *match_info,
-                              MMCallHuawei   *self)
-{
-    guint call_x    = 0;
-    guint call_type = 0;
-
-    if (!mm_get_uint_from_match_info (match_info, 1, &call_x))
-        return;
-
-    if (!mm_get_uint_from_match_info (match_info, 2, &call_type))
-        return;
-
-    mm_dbg ("Call id '%u' of type '%u' connected", call_x, call_type);
-
-    if (mm_gdbus_call_get_state (MM_GDBUS_CALL (self)) == MM_CALL_STATE_RINGING_OUT)
-        mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
-}
-
-static void
-huawei_voice_call_end (MMPortSerialAt *port,
-                       GMatchInfo     *match_info,
-                       MMCallHuawei   *self)
-{
-    guint call_x     = 0;
-    guint duration   = 0;
-    guint cc_cause   = 0;
-    guint end_status = 0;
-
-    if (!mm_get_uint_from_match_info (match_info, 1, &call_x))
-        return;
-
-    if (!mm_get_uint_from_match_info (match_info, 2, &duration))
-        return;
-
-    if (!mm_get_uint_from_match_info (match_info, 3, &end_status))
-        return;
-
-    /* This is optional */
-    mm_get_uint_from_match_info (match_info, 4, &cc_cause);
-
-    mm_dbg ("Call id '%u' terminated with status '%u' and cause '%u'. Duration of call '%d'",
-            call_x, end_status, cc_cause, duration);
-
-    mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);
-}
-
-static void
-huawei_voice_received_dtmf (MMPortSerialAt *port,
-                            GMatchInfo     *match_info,
-                            MMCallHuawei   *self)
-{
-    gchar *key;
-
-    key = g_match_info_fetch (match_info, 1);
-
-    if (!key)
-        return;
-
-    mm_dbg ("Received DTMF '%s'", key);
-    mm_base_call_received_dtmf (MM_BASE_CALL (self), key);
-    g_free (key);
-}
-
-static gboolean
-common_setup_cleanup_unsolicited_events (MMCallHuawei  *self,
-                                         gboolean       enable,
-                                         GError       **error)
-{
-    MMBaseModem *modem = NULL;
-    GList       *ports, *l;
-
-    if (G_UNLIKELY (!self->priv->conf_regex))
-        self->priv->conf_regex = g_regex_new ("\\r\\n\\^CONF:\\s*(\\d+)\\r\\n",
-                                              G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-    if (G_UNLIKELY (!self->priv->conn_regex))
-        self->priv->conn_regex = g_regex_new ("\\r\\n\\^CONN:\\s*(\\d+),(\\d+)\\r\\n",
-                                              G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-    if (G_UNLIKELY (!self->priv->cend_regex))
-        self->priv->cend_regex = g_regex_new ("\\r\\n\\^CEND:\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),?\\s*(\\d*)\\r\\n",
-                                              G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-    if (G_UNLIKELY (!self->priv->ddtmf_regex))
-        self->priv->ddtmf_regex = g_regex_new ("\\r\\n\\^DDTMF:\\s*([0-9A-D\\*\\#])\\r\\n",
-                                               G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-
-    g_object_get (self,
-                  MM_BASE_CALL_MODEM, &modem,
-                  NULL);
-
-    ports = mm_broadband_modem_huawei_get_at_port_list (MM_BROADBAND_MODEM_HUAWEI (modem));
-
-    for (l = ports; l; l = g_list_next (l)) {
-        MMPortSerialAt *port;
-
-        port = MM_PORT_SERIAL_AT (l->data);
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->conf_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)huawei_voice_ringback_tone : NULL,
-            enable ? self : NULL,
-            NULL);
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->conn_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)huawei_voice_call_connection : NULL,
-            enable ? self : NULL,
-            NULL);
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->cend_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)huawei_voice_call_end : NULL,
-            enable ? self : NULL,
-            NULL);
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->ddtmf_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)huawei_voice_received_dtmf: NULL,
-            enable ? self : NULL,
-            NULL);
-    }
-
-    g_list_free_full (ports, g_object_unref);
-    g_object_unref (modem);
-    return TRUE;
-}
-
-static gboolean
-setup_unsolicited_events (MMBaseCall  *self,
-                          GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (MM_CALL_HUAWEI (self), TRUE, error);
-}
-
-static gboolean
-cleanup_unsolicited_events (MMBaseCall  *self,
-                            GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (MM_CALL_HUAWEI (self), FALSE, error);
-}
-
-/*****************************************************************************/
-
-MMBaseCall *
-mm_call_huawei_new (MMBaseModem     *modem,
-                    MMCallDirection  direction,
-                    const gchar     *number,
-                    guint            audio_hz,
-                    guint            audio_bits)
-{
-    return MM_BASE_CALL (g_object_new (MM_TYPE_CALL_HUAWEI,
-                                       MM_BASE_CALL_MODEM,        modem,
-                                       "direction",               direction,
-                                       "number",                  number,
-                                       MM_CALL_HUAWEI_AUDIO_HZ,   audio_hz,
-                                       MM_CALL_HUAWEI_AUDIO_BITS, audio_bits,
-                                       MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, TRUE,
-                                       MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE,  TRUE,
-                                       NULL));
-}
-
-static void
-mm_call_huawei_init (MMCallHuawei *self)
-{
-    /* Initialize private data */
-    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_CALL_HUAWEI, MMCallHuaweiPrivate);
-}
-
-static void
-set_property (GObject      *object,
-              guint         prop_id,
-              const GValue *value,
-              GParamSpec   *pspec)
-{
-    MMCallHuawei *self = MM_CALL_HUAWEI (object);
-
-    switch (prop_id) {
-    case PROP_AUDIO_HZ:
-        self->priv->audio_hz = g_value_get_uint (value);
-        break;
-    case PROP_AUDIO_BITS:
-        self->priv->audio_bits = g_value_get_uint (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)
-{
-    MMCallHuawei *self = MM_CALL_HUAWEI (object);
-
-    switch (prop_id) {
-    case PROP_AUDIO_HZ:
-        g_value_set_uint (value, self->priv->audio_hz);
-        break;
-    case PROP_AUDIO_BITS:
-        g_value_set_uint (value, self->priv->audio_bits);
-        break;
-    default:
-        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-        break;
-    }
-}
-
-static void
-finalize (GObject *object)
-{
-    MMCallHuawei *self = MM_CALL_HUAWEI (object);
-
-    if (self->priv->conf_regex)
-        g_regex_unref (self->priv->conf_regex);
-    if (self->priv->conn_regex)
-        g_regex_unref (self->priv->conn_regex);
-    if (self->priv->cend_regex)
-        g_regex_unref (self->priv->cend_regex);
-    if (self->priv->ddtmf_regex)
-        g_regex_unref (self->priv->ddtmf_regex);
-
-    G_OBJECT_CLASS (mm_call_huawei_parent_class)->finalize (object);
-}
-
-static void
-mm_call_huawei_class_init (MMCallHuaweiClass *klass)
-{
-    GObjectClass *object_class = G_OBJECT_CLASS (klass);
-    MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass);
-
-    g_type_class_add_private (object_class, sizeof (MMCallHuaweiPrivate));
-
-    object_class->get_property = get_property;
-    object_class->set_property = set_property;
-    object_class->finalize     = finalize;
-
-    base_call_class->setup_unsolicited_events     = setup_unsolicited_events;
-    base_call_class->cleanup_unsolicited_events   = cleanup_unsolicited_events;
-    base_call_class->setup_audio_channel          = setup_audio_channel;
-    base_call_class->setup_audio_channel_finish   = setup_audio_channel_finish;
-
-    properties[PROP_AUDIO_HZ] =
-        g_param_spec_uint (MM_CALL_HUAWEI_AUDIO_HZ,
-                           "Audio Hz",
-                           "Voice call audio hz if call audio is routed via the host",
-                           0, 24000, 0,
-                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
-    g_object_class_install_property (object_class, PROP_AUDIO_HZ, properties[PROP_AUDIO_HZ]);
-
-    properties[PROP_AUDIO_BITS] =
-        g_param_spec_uint (MM_CALL_HUAWEI_AUDIO_BITS,
-                           "Audio Bits",
-                           "Voice call audio bits if call audio is routed via the host",
-                           0, 24, 0,
-                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
-    g_object_class_install_property (object_class, PROP_AUDIO_BITS, properties[PROP_AUDIO_BITS]);
-}
diff --git a/plugins/huawei/mm-call-huawei.h b/plugins/huawei/mm-call-huawei.h
deleted file mode 100644
index 1b70e81..0000000
--- a/plugins/huawei/mm-call-huawei.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/* -*- 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) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
- */
-
-#ifndef MM_CALL_HUAWEI_H
-#define MM_CALL_HUAWEI_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include "mm-base-call.h"
-
-#define MM_TYPE_CALL_HUAWEI            (mm_call_huawei_get_type ())
-#define MM_CALL_HUAWEI(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_CALL_HUAWEI, MMCallHuawei))
-#define MM_CALL_HUAWEI_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_CALL_HUAWEI, MMCallHuaweiClass))
-#define MM_IS_CALL_HUAWEI(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_CALL_HUAWEI))
-#define MM_IS_CALL_HUAWEI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_CALL_HUAWEI))
-#define MM_CALL_HUAWEI_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_CALL_HUAWEI, MMCallHuaweiClass))
-
-#define MM_CALL_HUAWEI_AUDIO_HZ   "call-huawei-audio-hz"
-#define MM_CALL_HUAWEI_AUDIO_BITS "call-huawei-audio-bits"
-
-typedef struct _MMCallHuawei MMCallHuawei;
-typedef struct _MMCallHuaweiClass MMCallHuaweiClass;
-typedef struct _MMCallHuaweiPrivate MMCallHuaweiPrivate;
-
-struct _MMCallHuawei {
-    MMBaseCall parent;
-    MMCallHuaweiPrivate *priv;
-};
-
-struct _MMCallHuaweiClass {
-    MMBaseCallClass parent;
-};
-
-GType mm_call_huawei_get_type (void);
-
-MMBaseCall *mm_call_huawei_new (MMBaseModem     *modem,
-                                MMCallDirection  direction,
-                                const gchar     *number,
-                                guint            audio_hz,
-                                guint            audio_bits);
-
-#endif /* MM_CALL_HUAWEI_H */
diff --git a/plugins/mbm/mm-broadband-modem-mbm.c b/plugins/mbm/mm-broadband-modem-mbm.c
index 06bc9ac..a4b89fd 100644
--- a/plugins/mbm/mm-broadband-modem-mbm.c
+++ b/plugins/mbm/mm-broadband-modem-mbm.c
@@ -1262,7 +1262,8 @@
             mm_port_serial_command (MM_PORT_SERIAL (gps_port),
                                     buf,
                                     3,
-                                    FALSE,
+                                    FALSE, /* never cached */
+                                    FALSE, /* always queued last */
                                     NULL,
                                     NULL,
                                     NULL);
diff --git a/plugins/quectel/77-mm-quectel-port-types.rules b/plugins/quectel/77-mm-quectel-port-types.rules
new file mode 100644
index 0000000..eb6c0a7
--- /dev/null
+++ b/plugins/quectel/77-mm-quectel-port-types.rules
@@ -0,0 +1,30 @@
+# do not edit this file, it will be overwritten on update
+ACTION!="add|change|move|bind", GOTO="mm_quectel_port_types_end"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c7c", GOTO="mm_quectel_port_types"
+GOTO="mm_quectel_port_types_end"
+
+LABEL="mm_quectel_port_types"
+
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# Quectel EG06
+#  ttyUSB0 (if #0): QCDM/DIAG port
+#  ttyUSB1 (if #1): GPS data port
+#  ttyUSB2 (if #2): AT primary port
+#  ttyUSB3 (if #3): AT secondary port
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0306", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_TYPE_QCDM}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0306", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_GPS}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0306", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0306", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+
+# Quectel EG91
+#  ttyUSB0 (if #0): QCDM/DIAG port
+#  ttyUSB1 (if #1): GPS data port
+#  ttyUSB2 (if #2): AT primary port
+#  ttyUSB3 (if #3): AT secondary port
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0191", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_QCDM}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0191", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_GPS}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0191", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0191", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+
+LABEL="mm_quectel_port_types_end"
diff --git a/plugins/quectel/mm-plugin-quectel.c b/plugins/quectel/mm-plugin-quectel.c
index 85d9314..383b08c 100644
--- a/plugins/quectel/mm-plugin-quectel.c
+++ b/plugins/quectel/mm-plugin-quectel.c
@@ -67,18 +67,18 @@
 mm_plugin_create (void)
 {
     static const gchar *subsystems[] = { "tty", "net", "usb", NULL };
+    static const gchar *vendor_strings[] = { "quectel", NULL };
     static const guint16 vendor_ids[] = { 0x2c7c, 0 };
-    static const gchar *drivers[] = { "qmi_wwan", NULL };
 
     return MM_PLUGIN (
         g_object_new (MM_TYPE_PLUGIN_QUECTEL,
-                      MM_PLUGIN_NAME,               "Quectel",
-                      MM_PLUGIN_ALLOWED_SUBSYSTEMS, subsystems,
-                      MM_PLUGIN_ALLOWED_VENDOR_IDS, vendor_ids,
-                      MM_PLUGIN_ALLOWED_DRIVERS,    drivers,
-                      MM_PLUGIN_ALLOWED_AT,         TRUE,
-                      MM_PLUGIN_ALLOWED_QCDM,       TRUE,
-                      MM_PLUGIN_ALLOWED_QMI,        TRUE,
+                      MM_PLUGIN_NAME,                   "Quectel",
+                      MM_PLUGIN_ALLOWED_SUBSYSTEMS,     subsystems,
+                      MM_PLUGIN_ALLOWED_VENDOR_IDS,     vendor_ids,
+                      MM_PLUGIN_ALLOWED_VENDOR_STRINGS, vendor_strings,
+                      MM_PLUGIN_ALLOWED_AT,             TRUE,
+                      MM_PLUGIN_ALLOWED_QCDM,           TRUE,
+                      MM_PLUGIN_ALLOWED_QMI,            TRUE,
                       NULL));
 }
 
diff --git a/plugins/sierra/77-mm-sierra.rules b/plugins/sierra/77-mm-sierra.rules
index d3da313..a976e60 100644
--- a/plugins/sierra/77-mm-sierra.rules
+++ b/plugins/sierra/77-mm-sierra.rules
@@ -5,8 +5,17 @@
 GOTO="mm_sierra_end"
 
 LABEL="mm_sierra"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
 
 # Netgear AC341U: enable connection status polling explicitly
 ATTRS{idVendor}=="1199", ATTRS{idProduct}=="9057", ENV{ID_MM_QMI_CONNECTION_STATUS_POLLING_ENABLE}="1"
 
-LABEL="mm_sierra_end"
\ No newline at end of file
+# MC74XX: Add port hints
+#  if 03: primary port
+#  if 02: raw NMEA port
+#  if 00: diag/qcdm port
+ATTRS{idVendor}=="1199", ATTRS{idProduct}=="9071", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="1199", ATTRS{idProduct}=="9071", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_PORT_TYPE_GPS}="1"
+ATTRS{idVendor}=="1199", ATTRS{idProduct}=="9071", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_TYPE_QCDM}="1"
+
+LABEL="mm_sierra_end"
diff --git a/plugins/simtech/mm-broadband-modem-simtech.c b/plugins/simtech/mm-broadband-modem-simtech.c
index c511629..b200114 100644
--- a/plugins/simtech/mm-broadband-modem-simtech.c
+++ b/plugins/simtech/mm-broadband-modem-simtech.c
@@ -209,7 +209,7 @@
 
 static const MMBaseModemAtCommand unsolicited_enable_sequence[] = {
     /* Autoreport access technology changes */
-    { "+CNSMOD=1",    5, FALSE, NULL },
+    { "+CNSMOD=1", 20, FALSE, NULL },
     /* Autoreport CSQ (first arg), and only report when it changes (second arg) */
     { "+AUTOCSQ=1,1", 5, FALSE, NULL },
     { NULL }
diff --git a/plugins/telit/77-mm-telit-port-types.rules b/plugins/telit/77-mm-telit-port-types.rules
index f8f9f08..6ec5a7e 100644
--- a/plugins/telit/77-mm-telit-port-types.rules
+++ b/plugins/telit/77-mm-telit-port-types.rules
@@ -33,4 +33,22 @@
 # CE910-DUAL
 ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1011", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
 
+# LE922
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1040", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1040", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1040", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1040", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1040", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+
+# ME910
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1101", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1101", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1101", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+
+# LM940/960 use alternate settings for 3G band management
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="0x1040", ENV{ID_MM_TELIT_BND_ALTERNATE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="0x1041", ENV{ID_MM_TELIT_BND_ALTERNATE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="0x1042", ENV{ID_MM_TELIT_BND_ALTERNATE}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="0x1043", ENV{ID_MM_TELIT_BND_ALTERNATE}="1"
+
 LABEL="mm_telit_port_types_end"
diff --git a/plugins/telit/mm-broadband-modem-mbim-telit.c b/plugins/telit/mm-broadband-modem-mbim-telit.c
index d4a8095..43bce7f 100644
--- a/plugins/telit/mm-broadband-modem-mbim-telit.c
+++ b/plugins/telit/mm-broadband-modem-mbim-telit.c
@@ -159,9 +159,9 @@
     iface->set_current_bands = mm_shared_telit_modem_set_current_bands;
     iface->set_current_bands_finish = mm_shared_telit_modem_set_current_bands_finish;
     iface->load_current_bands = mm_shared_telit_modem_load_current_bands;
-    iface->load_current_bands_finish = mm_shared_telit_modem_load_bands_finish;
+    iface->load_current_bands_finish = mm_shared_telit_modem_load_current_bands_finish;
     iface->load_supported_bands = mm_shared_telit_modem_load_supported_bands;
-    iface->load_supported_bands_finish = mm_shared_telit_modem_load_bands_finish;
+    iface->load_supported_bands_finish = mm_shared_telit_modem_load_supported_bands_finish;
     iface->load_supported_modes = load_supported_modes;
     iface->load_supported_modes_finish = load_supported_modes_finish;
     iface->load_current_modes = mm_shared_telit_load_current_modes;
diff --git a/plugins/telit/mm-broadband-modem-telit.c b/plugins/telit/mm-broadband-modem-telit.c
index dab0b7a..d6b1ea2 100644
--- a/plugins/telit/mm-broadband-modem-telit.c
+++ b/plugins/telit/mm-broadband-modem-telit.c
@@ -844,8 +844,8 @@
 }
 
 static const MMBaseModemAtCommand access_tech_commands[] = {
-    { "#PSNT?",  3, TRUE, response_processor_psnt_ignore_at_errors },
-    { "+SERVICE?", 3, TRUE, response_processor_service_ignore_at_errors },
+    { "#PSNT?",    3, FALSE, response_processor_psnt_ignore_at_errors },
+    { "+SERVICE?", 3, FALSE, response_processor_service_ignore_at_errors },
     { NULL }
 };
 
@@ -1031,9 +1031,9 @@
     iface->set_current_bands = mm_shared_telit_modem_set_current_bands;
     iface->set_current_bands_finish = mm_shared_telit_modem_set_current_bands_finish;
     iface->load_current_bands = mm_shared_telit_modem_load_current_bands;
-    iface->load_current_bands_finish = mm_shared_telit_modem_load_bands_finish;
+    iface->load_current_bands_finish = mm_shared_telit_modem_load_current_bands_finish;
     iface->load_supported_bands = mm_shared_telit_modem_load_supported_bands;
-    iface->load_supported_bands_finish = mm_shared_telit_modem_load_bands_finish;
+    iface->load_supported_bands_finish = mm_shared_telit_modem_load_supported_bands_finish;
     iface->load_unlock_retries_finish = modem_load_unlock_retries_finish;
     iface->load_unlock_retries = modem_load_unlock_retries;
     iface->reset = modem_reset;
diff --git a/plugins/telit/mm-modem-helpers-telit.c b/plugins/telit/mm-modem-helpers-telit.c
index c162964..e4bacc5 100644
--- a/plugins/telit/mm-modem-helpers-telit.c
+++ b/plugins/telit/mm-modem-helpers-telit.c
@@ -10,8 +10,8 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2015 Telit.
- *
+ * Copyright (C) 2015-2019 Telit.
+ * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include <stdlib.h>
@@ -27,115 +27,299 @@
 #include "mm-modem-helpers.h"
 #include "mm-modem-helpers-telit.h"
 
+/*****************************************************************************/
+/* AT#BND 2G values */
+
+#define MM_MODEM_BAND_TELIT_2G_FIRST MM_MODEM_BAND_EGSM
+#define MM_MODEM_BAND_TELIT_2G_LAST  MM_MODEM_BAND_G850
+
+#define B2G_FLAG(band) (1 << (band - MM_MODEM_BAND_TELIT_2G_FIRST))
+
+/* Index of the array is the telit 2G band value [0-5]
+ * The bitmask value here is built from the 2G MMModemBand values right away. */
+static const guint32 telit_2g_to_mm_band_mask[] = {
+    [0] = B2G_FLAG (MM_MODEM_BAND_EGSM) + B2G_FLAG (MM_MODEM_BAND_DCS),
+    [1] = B2G_FLAG (MM_MODEM_BAND_EGSM) + B2G_FLAG (MM_MODEM_BAND_PCS),
+    [2] = B2G_FLAG (MM_MODEM_BAND_DCS)  + B2G_FLAG (MM_MODEM_BAND_G850),
+    [3] = B2G_FLAG (MM_MODEM_BAND_PCS)  + B2G_FLAG (MM_MODEM_BAND_G850),
+    [4] = B2G_FLAG (MM_MODEM_BAND_EGSM) + B2G_FLAG (MM_MODEM_BAND_DCS) + B2G_FLAG (MM_MODEM_BAND_PCS),
+    [5] = B2G_FLAG (MM_MODEM_BAND_EGSM) + B2G_FLAG (MM_MODEM_BAND_DCS) + B2G_FLAG (MM_MODEM_BAND_PCS) + B2G_FLAG (MM_MODEM_BAND_G850),
+};
+
+/*****************************************************************************/
+/* AT#BND 3G values */
+
+/* NOTE: UTRAN_1 to UTRAN_9 enum values are NOT IN ORDER!
+ * E.g. numerically UTRAN_7 is after UTRAN_9...
+ *
+ * This array allows us to iterate them in a way which is a bit
+ * more friendly.
+ */
+static const guint band_utran_index[] = {
+    [MM_MODEM_BAND_UTRAN_1] = 1,
+    [MM_MODEM_BAND_UTRAN_2] = 2,
+    [MM_MODEM_BAND_UTRAN_3] = 3,
+    [MM_MODEM_BAND_UTRAN_4] = 4,
+    [MM_MODEM_BAND_UTRAN_5] = 5,
+    [MM_MODEM_BAND_UTRAN_6] = 6,
+    [MM_MODEM_BAND_UTRAN_7] = 7,
+    [MM_MODEM_BAND_UTRAN_8] = 8,
+    [MM_MODEM_BAND_UTRAN_9] = 9,
+    [MM_MODEM_BAND_UTRAN_10] = 10,
+    [MM_MODEM_BAND_UTRAN_11] = 11,
+    [MM_MODEM_BAND_UTRAN_12] = 12,
+    [MM_MODEM_BAND_UTRAN_13] = 13,
+    [MM_MODEM_BAND_UTRAN_14] = 14,
+    [MM_MODEM_BAND_UTRAN_19] = 19,
+    [MM_MODEM_BAND_UTRAN_20] = 20,
+    [MM_MODEM_BAND_UTRAN_21] = 21,
+    [MM_MODEM_BAND_UTRAN_22] = 22,
+    [MM_MODEM_BAND_UTRAN_25] = 25,
+    [MM_MODEM_BAND_UTRAN_26] = 26,
+    [MM_MODEM_BAND_UTRAN_32] = 32,
+};
+
+#define MM_MODEM_BAND_TELIT_3G_FIRST MM_MODEM_BAND_UTRAN_1
+#define MM_MODEM_BAND_TELIT_3G_LAST  MM_MODEM_BAND_UTRAN_19
+
+#define B3G_NUM(band) band_utran_index[band]
+#define B3G_FLAG(band) ((B3G_NUM (band) > 0) ? (1 << (B3G_NUM (band) - B3G_NUM (MM_MODEM_BAND_TELIT_3G_FIRST))) : 0)
+
+/* Index of the arrays is the telit 3G band value.
+ * The bitmask value here is built from the 3G MMModemBand values right away.
+ *
+ * We have 2 different sets of bands that are different for values >=12, because
+ * the LM940/960 models support a different set of 3G bands.
+ */
+
+#define TELIT_3G_TO_MM_BAND_MASK_DEFAULT_N_ELEMENTS 27
+static guint32 telit_3g_to_mm_band_mask_default[TELIT_3G_TO_MM_BAND_MASK_DEFAULT_N_ELEMENTS];
+
+#define TELIT_3G_TO_MM_BAND_MASK_ALTERNATE_N_ELEMENTS 20
+static guint32 telit_3g_to_mm_band_mask_alternate[TELIT_3G_TO_MM_BAND_MASK_ALTERNATE_N_ELEMENTS];
+
+static void
+initialize_telit_3g_to_mm_band_masks (void)
+{
+    static gboolean initialized = FALSE;
+
+    /* We need to initialize the arrays in runtime because gcc < 8 doesn't like initializing
+     * with operations that are using the band_utran_index constant array elements */
+
+    if (initialized)
+        return;
+
+    telit_3g_to_mm_band_mask_default[0]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1);
+    telit_3g_to_mm_band_mask_default[1]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_2);
+    telit_3g_to_mm_band_mask_default[2]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[3]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[4]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[5]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[6]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[7]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_4);
+    telit_3g_to_mm_band_mask_default[8]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[9]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[10] = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[11] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[12] = B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[13] = B3G_FLAG (MM_MODEM_BAND_UTRAN_3);
+    telit_3g_to_mm_band_mask_default[14] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[15] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[16] = B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[17] = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[18] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[19] = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[20] = B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[21] = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6);
+    telit_3g_to_mm_band_mask_default[22] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_default[23] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3);
+    telit_3g_to_mm_band_mask_default[24] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_default[25] = B3G_FLAG (MM_MODEM_BAND_UTRAN_19);
+    telit_3g_to_mm_band_mask_default[26] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6) +
+                                           B3G_FLAG (MM_MODEM_BAND_UTRAN_8) + B3G_FLAG (MM_MODEM_BAND_UTRAN_19);
+
+    /* Initialize alternate 3G band set */
+    telit_3g_to_mm_band_mask_alternate[0]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1);
+    telit_3g_to_mm_band_mask_alternate[1]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_2);
+    telit_3g_to_mm_band_mask_alternate[2]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[3]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[4]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[5]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[6]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[7]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_4);
+    telit_3g_to_mm_band_mask_alternate[8]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[9]  = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[10] = B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[11] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[12] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[13] = B3G_FLAG (MM_MODEM_BAND_UTRAN_3);
+    telit_3g_to_mm_band_mask_alternate[14] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[15] = B3G_FLAG (MM_MODEM_BAND_UTRAN_3) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5);
+    telit_3g_to_mm_band_mask_alternate[16] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_3) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_4) + B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[17] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8);
+    telit_3g_to_mm_band_mask_alternate[18] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8) + B3G_FLAG (MM_MODEM_BAND_UTRAN_9) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_19);
+    telit_3g_to_mm_band_mask_alternate[19] = B3G_FLAG (MM_MODEM_BAND_UTRAN_1) + B3G_FLAG (MM_MODEM_BAND_UTRAN_2) + B3G_FLAG (MM_MODEM_BAND_UTRAN_4) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_5) + B3G_FLAG (MM_MODEM_BAND_UTRAN_6) + B3G_FLAG (MM_MODEM_BAND_UTRAN_8) +
+                                             B3G_FLAG (MM_MODEM_BAND_UTRAN_9) + B3G_FLAG (MM_MODEM_BAND_UTRAN_19);
+}
+
+/*****************************************************************************/
+/* AT#BND 4G values
+ *
+ * The Telit-specific value for 4G bands is a bitmask of the band values, given
+ * in hexadecimal or decimal format.
+ */
+
+#define MM_MODEM_BAND_TELIT_4G_FIRST MM_MODEM_BAND_EUTRAN_1
+#define MM_MODEM_BAND_TELIT_4G_LAST  MM_MODEM_BAND_EUTRAN_44
+
+#define B4G_FLAG(band) (((guint64) 1) << (band - MM_MODEM_BAND_TELIT_4G_FIRST))
 
 /*****************************************************************************/
 /* Set current bands helpers */
 
-void
-mm_telit_get_band_flag (GArray *bands_array,
-                        gint *flag2g,
-                        gint *flag3g,
-                        gint *flag4g)
+gchar *
+mm_telit_build_bnd_request (GArray    *bands_array,
+                            gboolean   modem_is_2g,
+                            gboolean   modem_is_3g,
+                            gboolean   modem_is_4g,
+                            gboolean   modem_alternate_3g_bands,
+                            GError   **error)
 {
-    guint mask2g = 0;
-    guint mask3g = 0;
-    guint mask4g = 0;
-    guint found4g = FALSE;
-    guint i;
+    guint32        mask2g = 0;
+    guint32        mask3g = 0;
+    guint64        mask4g = 0;
+    guint          i;
+    gint           flag2g = -1;
+    gint           flag3g = -1;
+    gint           flag4g = -1;
+    gchar         *cmd;
+    const guint32 *telit_3g_to_mm_band_mask;
+    guint          telit_3g_to_mm_band_mask_n_elements;
+
+    initialize_telit_3g_to_mm_band_masks ();
+
+    /* Select correct 3G band mask */
+    if (modem_alternate_3g_bands) {
+        telit_3g_to_mm_band_mask = telit_3g_to_mm_band_mask_alternate;
+        telit_3g_to_mm_band_mask_n_elements = G_N_ELEMENTS (telit_3g_to_mm_band_mask_alternate);
+    } else {
+        telit_3g_to_mm_band_mask = telit_3g_to_mm_band_mask_default;
+        telit_3g_to_mm_band_mask_n_elements = G_N_ELEMENTS (telit_3g_to_mm_band_mask_default);
+    }
 
     for (i = 0; i < bands_array->len; i++) {
-        MMModemBand band = g_array_index (bands_array, MMModemBand, i);
+        MMModemBand band;
 
-        if (flag2g != NULL) {
-            switch (band) {
-                case MM_MODEM_BAND_EGSM:
-                case MM_MODEM_BAND_DCS:
-                case MM_MODEM_BAND_PCS:
-                case MM_MODEM_BAND_G850:
-                    mask2g += 1 << band;
-                    break;
-                default:
-                    break;
+        band = g_array_index (bands_array, MMModemBand, i);
+
+        /* Convert 2G bands into a bitmask, to match against telit_2g_to_mm_band_mask. */
+        if (flag2g && mm_common_band_is_gsm (band) &&
+            (band >= MM_MODEM_BAND_TELIT_2G_FIRST) && (band <= MM_MODEM_BAND_TELIT_2G_LAST))
+            mask2g += B2G_FLAG (band);
+
+        /* Convert 3G bands into a bitmask, to match against telit_3g_to_mm_band_mask. */
+        if (flag3g && mm_common_band_is_utran (band) &&
+            (B3G_NUM (band) >= B3G_NUM (MM_MODEM_BAND_TELIT_3G_FIRST)) && (B3G_NUM (band) <= B3G_NUM (MM_MODEM_BAND_TELIT_3G_LAST)))
+            mask3g += B3G_FLAG (band);
+
+        /* Convert 4G bands into a bitmask. We use a 64bit explicit bitmask so that
+         * all values fit correctly. */
+        if (flag4g && mm_common_band_is_eutran (band) &&
+            (band >= MM_MODEM_BAND_TELIT_4G_FIRST && band <= MM_MODEM_BAND_TELIT_4G_LAST))
+             mask4g += B4G_FLAG (band);
+    }
+
+    /* Get 2G-specific telit value */
+    if (mask2g) {
+        for (i = 0; i < G_N_ELEMENTS (telit_2g_to_mm_band_mask); i++) {
+            if (mask2g == telit_2g_to_mm_band_mask[i]) {
+                flag2g = i;
+                break;
             }
         }
+        if (flag2g == -1) {
+            gchar *bands_str;
 
-        if (flag3g != NULL) {
-            switch (band) {
-                case MM_MODEM_BAND_UTRAN_1:
-                case MM_MODEM_BAND_UTRAN_2:
-                case MM_MODEM_BAND_UTRAN_3:
-                case MM_MODEM_BAND_UTRAN_4:
-                case MM_MODEM_BAND_UTRAN_5:
-                case MM_MODEM_BAND_UTRAN_6:
-                case MM_MODEM_BAND_UTRAN_7:
-                case MM_MODEM_BAND_UTRAN_8:
-                case MM_MODEM_BAND_UTRAN_9:
-                    mask3g += 1 << band;
-                    break;
-                default:
-                    break;
+            bands_str = mm_common_build_bands_string ((const MMModemBand *)(bands_array->data), bands_array->len);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "Couldn't find matching 2G bands Telit value for band combination: '%s'", bands_str);
+            g_free (bands_str);
+            return NULL;
+        }
+    }
+
+    /* Get 3G-specific telit value */
+    if (mask3g) {
+        for (i = 0; i < telit_3g_to_mm_band_mask_n_elements; i++) {
+            if (mask3g == telit_3g_to_mm_band_mask[i]) {
+                flag3g = i;
+                break;
             }
         }
+        if (flag3g == -1) {
+            gchar *bands_str;
 
-         if (flag4g != NULL &&
-             band >= MM_MODEM_BAND_EUTRAN_1 && band <= MM_MODEM_BAND_EUTRAN_44) {
-             mask4g += 1 << (band - MM_MODEM_BAND_EUTRAN_1);
-             found4g = TRUE;
+            bands_str = mm_common_build_bands_string ((const MMModemBand *)(bands_array->data), bands_array->len);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "Couldn't find matching 3G bands Telit value for band combination: '%s'", bands_str);
+            g_free (bands_str);
+            return NULL;
         }
     }
 
-    /* Get 2G flag */
-    if (flag2g != NULL) {
-        if (mask2g == ((1 << MM_MODEM_BAND_EGSM) + (1 << MM_MODEM_BAND_DCS)))
-            *flag2g = 0;
-        else if (mask2g == ((1 << MM_MODEM_BAND_EGSM) + (1 << MM_MODEM_BAND_PCS)))
-            *flag2g = 1;
-        else if (mask2g == ((1 << MM_MODEM_BAND_G850) + (1 << MM_MODEM_BAND_DCS)))
-            *flag2g = 2;
-        else if (mask2g == ((1 << MM_MODEM_BAND_G850) + (1 << MM_MODEM_BAND_PCS)))
-            *flag2g = 3;
-        else
-            *flag2g = -1;
+    /* Get 4G-specific telit band bitmask */
+    flag4g = (mask4g != 0) ? mask4g : -1;
+
+    /* If the modem supports a given access tech, we must always give band settings
+     * for the specific tech */
+    if (modem_is_2g && flag2g == -1) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND,
+                     "None or invalid 2G bands combination in the provided list");
+        return NULL;
+    }
+    if (modem_is_3g && flag3g == -1) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND,
+                     "None or invalid 3G bands combination in the provided list");
+        return NULL;
+    }
+    if (modem_is_4g && flag4g == -1) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND,
+                     "None or invalid 4G bands combination in the provided list");
+        return NULL;
     }
 
-    /* Get 3G flag */
-    if (flag3g != NULL) {
-        if (mask3g == (1 << MM_MODEM_BAND_UTRAN_1))
-            *flag3g = 0;
-        else if (mask3g == (1 << MM_MODEM_BAND_UTRAN_2))
-            *flag3g = 1;
-        else if (mask3g == (1 << MM_MODEM_BAND_UTRAN_5))
-            *flag3g = 2;
-        else if (mask3g == ((1 << MM_MODEM_BAND_UTRAN_1) +
-                            (1 << MM_MODEM_BAND_UTRAN_2) +
-                            (1 << MM_MODEM_BAND_UTRAN_5)))
-            *flag3g = 3;
-        else if (mask3g == ((1 << MM_MODEM_BAND_UTRAN_2) +
-                            (1 << MM_MODEM_BAND_UTRAN_5)))
-            *flag3g = 4;
-        else if (mask3g == (1 << MM_MODEM_BAND_UTRAN_8))
-            *flag3g = 5;
-        else if (mask3g == ((1 << MM_MODEM_BAND_UTRAN_1) +
-                            (1 << MM_MODEM_BAND_UTRAN_8)))
-            *flag3g = 6;
-        else if (mask3g == (1 << MM_MODEM_BAND_UTRAN_4))
-            *flag3g = 7;
-        else
-            *flag3g = -1;
-    }
+    if (modem_is_2g && !modem_is_3g && !modem_is_4g)
+        cmd = g_strdup_printf ("#BND=%d", flag2g);
+    else if (!modem_is_2g && modem_is_3g && !modem_is_4g)
+        cmd = g_strdup_printf ("#BND=0,%d", flag3g);
+    else if (!modem_is_2g && !modem_is_3g && modem_is_4g)
+        cmd = g_strdup_printf ("#BND=0,0,%d", flag4g);
+    else if (modem_is_2g && modem_is_3g && !modem_is_4g)
+        cmd = g_strdup_printf ("#BND=%d,%d", flag2g, flag3g);
+    else if (!modem_is_2g && modem_is_3g && modem_is_4g)
+        cmd = g_strdup_printf ("#BND=0,%d,%d", flag3g, flag4g);
+    else if (modem_is_2g && !modem_is_3g && modem_is_4g)
+        cmd = g_strdup_printf ("#BND=%d,0,%d", flag2g, flag4g);
+    else if (modem_is_2g && modem_is_3g && modem_is_4g)
+        cmd = g_strdup_printf ("#BND=%d,%d,%d", flag2g, flag3g, flag4g);
+    else
+        g_assert_not_reached ();
 
-    /* 4G flag correspond to the mask */
-    if (flag4g != NULL) {
-        if (found4g)
-            *flag4g = mask4g;
-        else
-            *flag4g = -1;
-    }
+    return cmd;
 }
 
-#define SUPP_BAND_RESPONSE_REGEX          "#BND:\\s*\\((?P<Bands2G>[0-9\\-,]*)\\)(,\\s*\\((?P<Bands3G>[0-9\\-,]*)\\))?(,\\s*\\((?P<Bands4G>[0-9\\-,]*)\\))?"
-#define CURR_BAND_RESPONSE_REGEX          "#BND:\\s*(?P<Bands2G>\\d+)(,\\s*(?P<Bands3G>\\d+))?(,\\s*(?P<Bands4G>\\d+))?"
-
 /*****************************************************************************/
 /* #BND response parser
  *
@@ -187,333 +371,255 @@
  *  4 = 3G band flag 4 is U1900 + U850
  *
  */
-gboolean
-mm_telit_parse_bnd_response (const gchar *response,
-                             gboolean modem_is_2g,
-                             gboolean modem_is_3g,
-                             gboolean modem_is_4g,
-                             MMTelitLoadBandsType band_type,
-                             GArray **supported_bands,
-                             GError **error)
-{
-    GArray *bands = NULL;
-    GMatchInfo *match_info = NULL;
-    GRegex *r = NULL;
-    gboolean ret = FALSE;
 
-    switch (band_type) {
-        case LOAD_SUPPORTED_BANDS:
-            /* Parse #BND=? response */
-            r = g_regex_new (SUPP_BAND_RESPONSE_REGEX, G_REGEX_RAW, 0, NULL);
-            break;
-        case LOAD_CURRENT_BANDS:
-            /* Parse #BND? response */
-            r = g_regex_new (CURR_BAND_RESPONSE_REGEX, G_REGEX_RAW, 0, NULL);
-        default:
-            break;
+static gboolean
+telit_get_2g_mm_bands (GMatchInfo  *match_info,
+                       GArray     **bands,
+                       GError     **error)
+{
+    GError *inner_error = NULL;
+    GArray *values = NULL;
+    gchar  *match_str = NULL;
+    guint   i;
+
+    match_str = g_match_info_fetch_named (match_info, "Bands2G");
+    if (!match_str || match_str[0] == '\0') {
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Could not find 2G band values from response");
+        goto out;
     }
 
+    values = mm_parse_uint_list (match_str, &inner_error);
+    if (!values)
+        goto out;
+
+    for (i = 0; i < values->len; i++) {
+        guint value;
+
+        value = g_array_index (values, guint, i);
+        if (value < G_N_ELEMENTS (telit_2g_to_mm_band_mask)) {
+            guint j;
+
+            for (j = MM_MODEM_BAND_TELIT_2G_FIRST; j <= MM_MODEM_BAND_TELIT_2G_LAST; j++) {
+                if ((telit_2g_to_mm_band_mask[value] & B2G_FLAG (j)) && !mm_common_bands_garray_lookup (*bands, j))
+                    *bands = g_array_append_val (*bands, j);
+            }
+        } else
+            mm_dbg ("unhandled telit 2G band value configuration: %u", value);
+    }
+
+out:
+    g_free (match_str);
+    g_clear_pointer (&values, g_array_unref);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+telit_get_3g_mm_bands (GMatchInfo  *match_info,
+                       GArray     **bands,
+                       gboolean     modem_alternate_3g_bands,
+                       GError     **error)
+{
+    GError        *inner_error = NULL;
+    GArray        *values = NULL;
+    gchar         *match_str = NULL;
+    guint          i;
+    const guint32 *telit_3g_to_mm_band_mask;
+    guint          telit_3g_to_mm_band_mask_n_elements;
+
+    initialize_telit_3g_to_mm_band_masks ();
+
+    /* Select correct 3G band mask */
+    if (modem_alternate_3g_bands) {
+        telit_3g_to_mm_band_mask = telit_3g_to_mm_band_mask_alternate;
+        telit_3g_to_mm_band_mask_n_elements = G_N_ELEMENTS (telit_3g_to_mm_band_mask_alternate);
+    } else {
+        telit_3g_to_mm_band_mask = telit_3g_to_mm_band_mask_default;
+        telit_3g_to_mm_band_mask_n_elements = G_N_ELEMENTS (telit_3g_to_mm_band_mask_default);
+    }
+
+    match_str = g_match_info_fetch_named (match_info, "Bands3G");
+    if (!match_str || match_str[0] == '\0') {
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Could not find 3G band values from response");
+        goto out;
+    }
+
+    values = mm_parse_uint_list (match_str, &inner_error);
+    if (!values)
+        goto out;
+
+    for (i = 0; i < values->len; i++) {
+        guint value;
+
+        value = g_array_index (values, guint, i);
+
+        if (value < telit_3g_to_mm_band_mask_n_elements) {
+            guint j;
+
+            for (j = 0; j < G_N_ELEMENTS (band_utran_index); j++) {
+                /* ignore non-3G bands */
+                if (band_utran_index[j] == 0)
+                    continue;
+
+                if ((telit_3g_to_mm_band_mask[value] & B3G_FLAG (j)) && !mm_common_bands_garray_lookup (*bands, j))
+                    *bands = g_array_append_val (*bands, j);
+            }
+        } else
+            mm_dbg ("unhandled telit 3G band value configuration: %u", value);
+    }
+
+out:
+    g_free (match_str);
+    g_clear_pointer (&values, g_array_unref);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+telit_get_4g_mm_bands (GMatchInfo  *match_info,
+                       GArray     **bands,
+                       GError     **error)
+{
+    GError       *inner_error = NULL;
+    MMModemBand   band;
+    gchar        *match_str = NULL;
+    guint64       value;
+    gchar       **tokens = NULL;
+
+    match_str = g_match_info_fetch_named (match_info, "Bands4G");
+    if (!match_str || match_str[0] == '\0') {
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Could not find 4G band flags from response");
+        goto out;
+    }
+
+    /* splitting will never return NULL as string is not empty */
+    tokens = g_strsplit (match_str, "-", -1);
+
+    /* If this is a range, get upper threshold, which contains the total supported mask */
+    if (!mm_get_u64_from_str (tokens[1] ? tokens[1] : tokens[0], &value)) {
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Could not parse 4G band mask from string: '%s'", match_str);
+        goto out;
+    }
+
+    for (band = MM_MODEM_BAND_TELIT_4G_FIRST; band <= MM_MODEM_BAND_TELIT_4G_LAST; band++) {
+        if ((value & B4G_FLAG (band)) && !mm_common_bands_garray_lookup (*bands, band))
+            g_array_append_val (*bands, band);
+    }
+
+out:
+    g_strfreev (tokens);
+    g_free (match_str);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+    return TRUE;
+}
+
+typedef enum {
+    LOAD_BANDS_TYPE_SUPPORTED,
+    LOAD_BANDS_TYPE_CURRENT,
+} LoadBandsType;
+
+static GArray *
+common_parse_bnd_response (const gchar    *response,
+                           gboolean        modem_is_2g,
+                           gboolean        modem_is_3g,
+                           gboolean        modem_is_4g,
+                           gboolean        modem_alternate_3g_bands,
+                           LoadBandsType   load_type,
+                           GError        **error)
+{
+    GError     *inner_error = NULL;
+    GArray     *bands = NULL;
+    GMatchInfo *match_info = NULL;
+    GRegex     *r;
+
+    static const gchar *load_bands_regex[] = {
+        [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:\\s*\\((?P<Bands2G>[0-9\\-,]*)\\)(,\\s*\\((?P<Bands3G>[0-9\\-,]*)\\))?(,\\s*\\((?P<Bands4G>[0-9\\-,]*)\\))?",
+        [LOAD_BANDS_TYPE_CURRENT]   = "#BND:\\s*(?P<Bands2G>\\d+)(,\\s*(?P<Bands3G>\\d+))?(,\\s*(?P<Bands4G>\\d+))?",
+    };
+
+    r = g_regex_new (load_bands_regex[load_type], G_REGEX_RAW, 0, NULL);
+    g_assert (r);
 
     if (!g_regex_match (r, response, 0, &match_info)) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not parse response '%s'", response);
-        goto end;
+        goto out;
     }
 
     if (!g_match_info_matches (match_info)) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not find matches in response '%s'", response);
-        goto end;
+        goto out;
     }
 
     bands = g_array_new (TRUE, TRUE, sizeof (MMModemBand));
 
-    if (modem_is_2g && !mm_telit_get_2g_mm_bands (match_info, &bands, error))
-        goto end;
+    if (modem_is_2g && !telit_get_2g_mm_bands (match_info, &bands, &inner_error))
+        goto out;
 
-    if (modem_is_3g && !mm_telit_get_3g_mm_bands (match_info, &bands, error))
-        goto end;
+    if (modem_is_3g && !telit_get_3g_mm_bands (match_info, &bands, modem_alternate_3g_bands, &inner_error))
+        goto out;
 
-    if (modem_is_4g && !mm_telit_get_4g_mm_bands (match_info, &bands, error))
-        goto end;
+    if (modem_is_4g && !telit_get_4g_mm_bands (match_info, &bands, &inner_error))
+        goto out;
 
-    *supported_bands = bands;
-    ret = TRUE;
-
-end:
-    if (!ret && bands != NULL)
-        g_array_free (bands, TRUE);
-
+out:
     g_match_info_free (match_info);
     g_regex_unref (r);
 
-    return ret;
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        g_clear_pointer (&bands, g_array_unref);
+        return NULL;
+    }
+
+    return bands;
 }
 
-gboolean
-mm_telit_get_2g_mm_bands (GMatchInfo *match_info,
-                          GArray **bands,
-                          GError **error)
+GArray *
+mm_telit_parse_bnd_query_response (const gchar  *response,
+                                   gboolean      modem_is_2g,
+                                   gboolean      modem_is_3g,
+                                   gboolean      modem_is_4g,
+                                   gboolean      modem_alternate_3g_bands,
+                                   GError      **error)
 {
-    GArray *flags = NULL;
-    gchar *match_str = NULL;
-    guint i;
-    gboolean ret = TRUE;
-
-    TelitToMMBandMap map [5] = {
-        { BND_FLAG_GSM900_DCS1800, {MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_UNKNOWN} }, /* 0 */
-        { BND_FLAG_GSM900_PCS1900, {MM_MODEM_BAND_EGSM, MM_MODEM_BAND_PCS, MM_MODEM_BAND_UNKNOWN} }, /* 1 */
-        { BND_FLAG_GSM850_DCS1800, {MM_MODEM_BAND_DCS, MM_MODEM_BAND_G850, MM_MODEM_BAND_UNKNOWN} }, /* 2 */
-        { BND_FLAG_GSM850_PCS1900, {MM_MODEM_BAND_PCS, MM_MODEM_BAND_G850, MM_MODEM_BAND_UNKNOWN} }, /* 3 */
-        { BND_FLAG_UNKNOWN, {}},
-    };
-
-    match_str = g_match_info_fetch_named (match_info, "Bands2G");
-
-    if (match_str == NULL || match_str[0] == '\0') {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                     "Could not find 2G band flags from response");
-        ret = FALSE;
-        goto end;
-    }
-
-    flags = g_array_new (FALSE, FALSE, sizeof (guint));
-
-    if (!mm_telit_get_band_flags_from_string (match_str, &flags, error)) {
-        ret = FALSE;
-        goto end;
-    }
-
-    for (i = 0; i < flags->len; i++) {
-        guint flag;
-
-        flag = g_array_index (flags, guint, i);
-        if (!mm_telit_update_band_array (flag, map, bands, error)) {
-            ret = FALSE;
-            goto end;
-        }
-    }
-
-end:
-    g_free (match_str);
-
-    if (flags != NULL)
-        g_array_free (flags, TRUE);
-
-    return ret;
+    return common_parse_bnd_response (response,
+                                      modem_is_2g, modem_is_3g, modem_is_4g,
+                                      modem_alternate_3g_bands,
+                                      LOAD_BANDS_TYPE_CURRENT,
+                                      error);
 }
 
-gboolean
-mm_telit_get_3g_mm_bands (GMatchInfo *match_info,
-                          GArray **bands,
-                          GError **error)
+GArray *
+mm_telit_parse_bnd_test_response (const gchar  *response,
+                                  gboolean      modem_is_2g,
+                                  gboolean      modem_is_3g,
+                                  gboolean      modem_is_4g,
+                                  gboolean      modem_alternate_3g_bands,
+                                  GError      **error)
 {
-    GArray *flags = NULL;
-    gchar *match_str = NULL;
-    guint i;
-    gboolean ret = TRUE;
-
-    TelitToMMBandMap map [] = {
-        { BND_FLAG_0, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_1, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_2, { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_3, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_4, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_5, { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_6, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_7, { MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UNKNOWN} },
-        { BND_FLAG_8, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_9, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_10, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_12, { MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN}},
-        { BND_FLAG_13, { MM_MODEM_BAND_UTRAN_3, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_14, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_15, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_3, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_16, { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_17, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_18, { MM_MODEM_BAND_UTRAN_1, MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN}},
-        { BND_FLAG_19, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN }},
-        { BND_FLAG_20, { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN}},
-        { BND_FLAG_21, { MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UNKNOWN}},
-        { BND_FLAG_UNKNOWN, {}},
-    };
-
-    match_str = g_match_info_fetch_named (match_info, "Bands3G");
-
-    if (match_str == NULL || match_str[0] == '\0') {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                     "Could not find 3G band flags from response");
-        ret = FALSE;
-        goto end;
-    }
-
-    flags = g_array_new (FALSE, FALSE, sizeof (guint));
-
-    if (!mm_telit_get_band_flags_from_string (match_str, &flags, error)) {
-        ret = FALSE;
-        goto end;
-    }
-
-    for (i = 0; i < flags->len; i++) {
-        guint flag;
-
-        flag = g_array_index (flags, guint, i);
-        if (!mm_telit_update_band_array (flag, map, bands, error)) {
-            ret = FALSE;
-            goto end;
-        }
-    }
-
-end:
-    g_free (match_str);
-
-    if (flags != NULL)
-        g_array_free (flags, TRUE);
-
-    return ret;
-}
-
-gboolean
-mm_telit_get_4g_mm_bands (GMatchInfo *match_info,
-                          GArray **bands,
-                          GError **error)
-{
-    MMModemBand band;
-    gboolean ret = TRUE;
-    gchar *match_str = NULL;
-    guint i;
-    guint value;
-    gchar **tokens;
-
-    match_str = g_match_info_fetch_named (match_info, "Bands4G");
-
-    if (match_str == NULL || match_str[0] == '\0') {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                     "Could not find 4G band flags from response");
-        ret = FALSE;
-        goto end;
-    }
-
-    if (strstr (match_str, "-")) {
-        tokens = g_strsplit (match_str, "-", -1);
-        if (tokens == NULL) {
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                         "Could not get 4G band ranges from string '%s'",
-                         match_str);
-            ret = FALSE;
-            goto end;
-        }
-        sscanf (tokens[1], "%d", &value);
-        g_strfreev (tokens);
-    } else {
-        sscanf (match_str, "%d", &value);
-    }
-
-    for (i = 0; value > 0; i++) {
-        if (value % 2 != 0) {
-            band = MM_MODEM_BAND_EUTRAN_1 + i;
-            g_array_append_val (*bands, band);
-        }
-        value = value >> 1;
-    }
-
-end:
-    g_free (match_str);
-
-    return ret;
-}
-
-gboolean
-mm_telit_bands_contains (GArray *mm_bands, const MMModemBand mm_band)
-{
-    guint i;
-
-    for (i = 0; i < mm_bands->len; i++) {
-        if (mm_band == g_array_index (mm_bands, MMModemBand, i))
-            return TRUE;
-    }
-
-    return FALSE;
-}
-
-gboolean
-mm_telit_update_band_array (const gint bands_flag,
-                            const TelitToMMBandMap *map,
-                            GArray **bands,
-                            GError **error)
-{
-    guint i;
-    guint j;
-
-    for (i = 0; map[i].flag != BND_FLAG_UNKNOWN; i++) {
-        if (bands_flag == map[i].flag) {
-            for (j = 0; map[i].mm_bands[j] != MM_MODEM_BAND_UNKNOWN; j++) {
-                if (!mm_telit_bands_contains (*bands, map[i].mm_bands[j])) {
-                    g_array_append_val (*bands, map[i].mm_bands[j]);
-                }
-            }
-
-            return TRUE;
-        }
-    }
-
-    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                 "No MM band found for Telit #BND flag '%d'",
-                 bands_flag);
-
-    return FALSE;
-}
-
-
-gboolean
-mm_telit_get_band_flags_from_string (const gchar *flag_str,
-                                     GArray **band_flags,
-                                     GError **error)
-{
-    gchar **range;
-    gchar **tokens;
-    guint flag;
-    guint i;
-
-    if (flag_str == NULL || flag_str[0] == '\0') {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                     "String is empty, no band flags to parse");
-        return FALSE;
-    }
-
-    tokens = g_strsplit (flag_str, ",", -1);
-    if (!tokens) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                     "Could not get the list of flags");
-        return FALSE;
-    }
-
-    for (i = 0; tokens[i]; i++) {
-        /* check whether tokens[i] defines a
-         * single band value or a range of bands */
-        if (!strstr (tokens[i], "-")) {
-            sscanf (tokens[i], "%d", &flag);
-            g_array_append_val (*band_flags, flag);
-        } else {
-            gint range_start;
-            gint range_end;
-
-            range = g_strsplit (tokens[i], "-", 2);
-
-            sscanf (range[0], "%d", &range_start);
-            sscanf (range[1], "%d", &range_end);
-
-            for (flag = range_start; flag <= range_end; flag++) {
-                g_array_append_val (*band_flags, flag);
-            }
-
-            g_strfreev (range);
-        }
-    }
-
-    g_strfreev (tokens);
-
-    return TRUE;
+    return common_parse_bnd_response (response,
+                                      modem_is_2g, modem_is_3g, modem_is_4g,
+                                      modem_alternate_3g_bands,
+                                      LOAD_BANDS_TYPE_SUPPORTED,
+                                      error);
 }
 
 /*****************************************************************************/
diff --git a/plugins/telit/mm-modem-helpers-telit.h b/plugins/telit/mm-modem-helpers-telit.h
index 0b82c42..491e8fa 100644
--- a/plugins/telit/mm-modem-helpers-telit.h
+++ b/plugins/telit/mm-modem-helpers-telit.h
@@ -10,8 +10,8 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2015 Telit.
- *
+ * Copyright (C) 2015-2019 Telit.
+ * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 #ifndef MM_MODEM_HELPERS_TELIT_H
 #define MM_MODEM_HELPERS_TELIT_H
@@ -19,81 +19,25 @@
 #include <glib.h>
 #include "ModemManager.h"
 
-#define MAX_BANDS_LIST_LEN 20
-
-#define BND_FLAG_UNKNOWN -1
-
-/* AT#BND 2G flags */
-typedef enum {
-    BND_FLAG_GSM900_DCS1800,
-    BND_FLAG_GSM900_PCS1900,
-    BND_FLAG_GSM850_DCS1800,
-    BND_FLAG_GSM850_PCS1900,
-} BndFlag2G;
-
-/* AT#BND 3G flags */
-typedef enum {
-    BND_FLAG_0,    /* B1 (2100 MHz) */
-    BND_FLAG_1,    /* B2 (1900 MHz) */
-    BND_FLAG_2,    /* B5 (850 MHz) */
-    BND_FLAG_3,    /* B1 (2100 MHz) + B2 (1900 MHz) + B5 (850 MHz) */
-    BND_FLAG_4,    /* B2 (1900 MHz) + B5 (850 MHz) */
-    BND_FLAG_5,    /* B8 (900 MHz) */
-    BND_FLAG_6,    /* B1 (2100 MHz) + B8 (900 MHz) */
-    BND_FLAG_7,    /* B4 (1700 MHz) */
-    BND_FLAG_8,    /* B1 (2100 MHz) + B5 (850 MHz) */
-    BND_FLAG_9,    /* B1 (2100 MHz) + B8 (900 MHz) + B5 (850 MHz) */
-    BND_FLAG_10,   /* B2 (1900 MHz) + B4 (1700 MHz) + B5 (850 MHz) */
-    BND_FLAG_12,   /* B6 (800 MHz) */
-    BND_FLAG_13,   /* B3 (1800 MHz) */
-    BND_FLAG_14,   /* B1 (2100 MHz) + B2 (1900 MHz) + B4 (1700 MHz) + B5 (850 MHz) + B6 (800MHz) */
-    BND_FLAG_15,   /* B1 (2100 MHz) + B8 (900 MHz) + B3 (1800 MHz) */
-    BND_FLAG_16,   /* B8 (900 MHz) + B5 (850 MHz) */
-    BND_FLAG_17,   /* B2 (1900 MHz) + B4 (1700 MHz) + B5 (850 MHz) + B6 (800 MHz) */
-    BND_FLAG_18,   /* B1 (2100 MHz) + B2 (1900 MHz) + B5 (850 MHz) + B6 (800 MHz) */
-    BND_FLAG_19,   /* B2 (1900 MHz) + B6 (800 MHz) */
-    BND_FLAG_20,   /* B5 (850 MHz) + B6 (800 MHz) */
-    BND_FLAG_21,   /* B2 (1900 MHz) + B5 (850 MHz) + B6 (800 MHz) */
-} BndFlag3G;
-
-typedef struct {
-    gint flag;
-    MMModemBand mm_bands[MAX_BANDS_LIST_LEN];
-} TelitToMMBandMap;
-
-typedef enum {
-    LOAD_SUPPORTED_BANDS,
-    LOAD_CURRENT_BANDS
-} MMTelitLoadBandsType;
-
-/* #BND response parser */
-gboolean
-mm_telit_parse_bnd_response (const gchar *response,
-                             gboolean modem_is_2g,
-                             gboolean modem_is_3g,
-                             gboolean modem_is_4g,
-                             MMTelitLoadBandsType band_type,
-                             GArray **supported_bands,
-                             GError **error);
-
-
-gboolean mm_telit_bands_contains (GArray *mm_bands, const MMModemBand mm_band);
-
-gboolean mm_telit_update_band_array (const gint bands_flag,
-                                     const TelitToMMBandMap *map,
-                                     GArray **bands,
-                                     GError **error);
-
-gboolean mm_telit_get_band_flags_from_string (const gchar *flag_str, GArray **band_flags, GError **error);
-gboolean mm_telit_get_2g_mm_bands(GMatchInfo *match_info, GArray **bands, GError **error);
-gboolean mm_telit_get_3g_mm_bands(GMatchInfo *match_info, GArray **bands, GError **error);
-gboolean mm_telit_get_4g_mm_bands(GMatchInfo *match_info, GArray **bands, GError **error);
-
-gboolean mm_telit_update_2g_bands(gchar *band_list, GMatchInfo **match_info, GArray **bands, GError **error);
-gboolean mm_telit_update_3g_bands(gchar *band_list, GMatchInfo **match_info, GArray **bands, GError **error);
-gboolean mm_telit_update_4g_bands(GArray** bands, GMatchInfo *match_info, GError **error);
-
-void mm_telit_get_band_flag (GArray *bands_array, gint *flag_2g, gint *flag_3g, gint *flag_4g);
+/* #BND response parsers and request builder */
+GArray *mm_telit_parse_bnd_query_response (const gchar  *response,
+                                           gboolean      modem_is_2g,
+                                           gboolean      modem_is_3g,
+                                           gboolean      modem_is_4g,
+                                           gboolean      modem_alternate_3g_bands,
+                                           GError      **error);
+GArray *mm_telit_parse_bnd_test_response  (const gchar  *response,
+                                           gboolean      modem_is_2g,
+                                           gboolean      modem_is_3g,
+                                           gboolean      modem_is_4g,
+                                           gboolean      modem_alternate_3g_bands,
+                                           GError      **error);
+gchar  *mm_telit_build_bnd_request        (GArray       *bands_array,
+                                           gboolean      modem_is_2g,
+                                           gboolean      modem_is_3g,
+                                           gboolean      modem_is_4g,
+                                           gboolean      modem_alternate_3g_bands,
+                                           GError      **error);
 
 /* #QSS? response parser */
 typedef enum { /*< underscore_name=mm_telit_qss_status >*/
diff --git a/plugins/telit/mm-shared-telit.c b/plugins/telit/mm-shared-telit.c
index c79f7ff..f1eb051 100644
--- a/plugins/telit/mm-shared-telit.c
+++ b/plugins/telit/mm-shared-telit.c
@@ -32,6 +32,59 @@
 #include "mm-shared-telit.h"
 
 /*****************************************************************************/
+/* Private data context */
+
+#define PRIVATE_TAG "shared-telit-private-tag"
+static GQuark private_quark;
+
+typedef struct {
+    gboolean  alternate_3g_bands;
+    GArray   *supported_bands;
+} Private;
+
+static void
+private_free (Private *priv)
+{
+    if (priv->supported_bands)
+        g_array_unref (priv->supported_bands);
+    g_slice_free (Private, priv);
+}
+
+static void
+initialize_alternate_3g_band (MMSharedTelit *self,
+                              Private       *priv)
+{
+    MMPort         *primary;
+    MMKernelDevice *port;
+
+    primary = MM_PORT (mm_base_modem_peek_port_primary (MM_BASE_MODEM (self)));
+    port = mm_port_peek_kernel_device (primary);
+
+    /* Lookup for the tag specifying that we're using the alternate 3G band mapping */
+    priv->alternate_3g_bands = mm_kernel_device_get_global_property_as_boolean (port, "ID_MM_TELIT_BND_ALTERNATE");
+    if (priv->alternate_3g_bands)
+        mm_dbg ("Telit modem using alternate 3G band mask setup");
+}
+
+static Private *
+get_private (MMSharedTelit *self)
+{
+    Private *priv;
+
+    if (G_UNLIKELY (!private_quark))
+        private_quark = g_quark_from_static_string (PRIVATE_TAG);
+
+    priv = g_object_get_qdata (G_OBJECT (self), private_quark);
+    if (!priv) {
+        priv = g_slice_new0 (Private);
+        initialize_alternate_3g_band (self, priv);
+        g_object_set_qdata_full (G_OBJECT (self), private_quark, priv, (GDestroyNotify)private_free);
+    }
+
+    return priv;
+}
+
+/*****************************************************************************/
 /* Load current mode (Modem interface) */
 
 gboolean
@@ -114,126 +167,133 @@
 /*****************************************************************************/
 /* Load supported bands (Modem interface) */
 
-typedef struct {
-    gboolean mm_modem_is_2g;
-    gboolean mm_modem_is_3g;
-    gboolean mm_modem_is_4g;
-    MMTelitLoadBandsType band_type;
-} LoadBandsContext;
-
-static void
-mm_shared_telit_load_bands_context_free (LoadBandsContext *ctx)
+GArray *
+mm_shared_telit_modem_load_supported_bands_finish (MMIfaceModem  *self,
+                                                   GAsyncResult  *res,
+                                                   GError       **error)
 {
-    g_slice_free (LoadBandsContext, ctx);
+    return g_task_propagate_pointer (G_TASK (res), error);
 }
 
 static void
-mm_shared_telit_load_bands_ready (MMBaseModem *self,
-                                  GAsyncResult *res,
-                                  GTask *task)
+mm_shared_telit_load_supported_bands_ready (MMBaseModem  *self,
+                                            GAsyncResult *res,
+                                            GTask        *task)
 {
     const gchar *response;
-    GError *error = NULL;
-    GArray *bands = NULL;
-    LoadBandsContext *ctx;
+    GError      *error = NULL;
+    Private     *priv;
 
-    ctx = g_task_get_task_data (task);
+    priv = get_private (MM_SHARED_TELIT (self));
+
     response = mm_base_modem_at_command_finish (self, res, &error);
-
     if (!response)
         g_task_return_error (task, error);
-    else if (!mm_telit_parse_bnd_response (response,
-                                           ctx->mm_modem_is_2g,
-                                           ctx->mm_modem_is_3g,
-                                           ctx->mm_modem_is_4g,
-                                           ctx->band_type,
-                                           &bands,
-                                           &error))
-        g_task_return_error (task, error);
-    else
-        g_task_return_pointer (task, bands, (GDestroyNotify)g_array_unref);
+    else {
+        GArray *bands;
+
+        bands = mm_telit_parse_bnd_test_response (response,
+                                                  mm_iface_modem_is_2g (MM_IFACE_MODEM (self)),
+                                                  mm_iface_modem_is_3g (MM_IFACE_MODEM (self)),
+                                                  mm_iface_modem_is_4g (MM_IFACE_MODEM (self)),
+                                                  priv->alternate_3g_bands,
+                                                  &error);
+        if (!bands)
+            g_task_return_error (task, error);
+        else {
+            /* Store supported bands to be able to build ANY when setting */
+            priv->supported_bands = g_array_ref (bands);
+            g_task_return_pointer (task, bands, (GDestroyNotify)g_array_unref);
+        }
+    }
 
     g_object_unref (task);
 }
 
 void
-mm_shared_telit_modem_load_supported_bands (MMIfaceModem *self,
-                                            GAsyncReadyCallback callback,
-                                            gpointer user_data)
+mm_shared_telit_modem_load_supported_bands (MMIfaceModem        *self,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data)
 {
-    GTask *task;
-    LoadBandsContext *ctx;
-
-    ctx = g_slice_new0 (LoadBandsContext);
-
-    ctx->mm_modem_is_2g = mm_iface_modem_is_2g (self);
-    ctx->mm_modem_is_3g = mm_iface_modem_is_3g (self);
-    ctx->mm_modem_is_4g = mm_iface_modem_is_4g (self);
-    ctx->band_type = LOAD_SUPPORTED_BANDS;
-
-    task = g_task_new (self, NULL, callback, user_data);
-    g_task_set_task_data (task, ctx, (GDestroyNotify)mm_shared_telit_load_bands_context_free);
-
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               "#BND=?",
                               3,
-                              FALSE,
-                              (GAsyncReadyCallback) mm_shared_telit_load_bands_ready,
-                              task);
+                              TRUE,
+                              (GAsyncReadyCallback) mm_shared_telit_load_supported_bands_ready,
+                              g_task_new (self, NULL, callback, user_data));
 }
 
 /*****************************************************************************/
 /* Load current bands (Modem interface) */
 
 GArray *
-mm_shared_telit_modem_load_bands_finish (MMIfaceModem *self,
-                                         GAsyncResult *res,
-                                         GError **error)
+mm_shared_telit_modem_load_current_bands_finish (MMIfaceModem  *self,
+                                                 GAsyncResult  *res,
+                                                 GError       **error)
 {
-    return (GArray *) g_task_propagate_pointer (G_TASK (res), error);
+    return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+static void
+mm_shared_telit_load_current_bands_ready (MMBaseModem  *self,
+                                          GAsyncResult *res,
+                                          GTask        *task)
+{
+    const gchar *response;
+    GError      *error = NULL;
+    Private     *priv;
+
+    priv = get_private (MM_SHARED_TELIT (self));
+
+    response = mm_base_modem_at_command_finish (self, res, &error);
+    if (!response)
+        g_task_return_error (task, error);
+    else {
+        GArray *bands;
+
+        bands = mm_telit_parse_bnd_query_response (response,
+                                                   mm_iface_modem_is_2g (MM_IFACE_MODEM (self)),
+                                                   mm_iface_modem_is_3g (MM_IFACE_MODEM (self)),
+                                                   mm_iface_modem_is_4g (MM_IFACE_MODEM (self)),
+                                                   priv->alternate_3g_bands,
+                                                   &error);
+        if (!bands)
+            g_task_return_error (task, error);
+        else
+            g_task_return_pointer (task, bands, (GDestroyNotify)g_array_unref);
+    }
+
+    g_object_unref (task);
 }
 
 void
-mm_shared_telit_modem_load_current_bands (MMIfaceModem *self,
-                                          GAsyncReadyCallback callback,
-                                          gpointer user_data)
+mm_shared_telit_modem_load_current_bands (MMIfaceModem        *self,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
 {
-    GTask *task;
-    LoadBandsContext *ctx;
-
-    ctx = g_slice_new0 (LoadBandsContext);
-
-    ctx->mm_modem_is_2g = mm_iface_modem_is_2g (self);
-    ctx->mm_modem_is_3g = mm_iface_modem_is_3g (self);
-    ctx->mm_modem_is_4g = mm_iface_modem_is_4g (self);
-    ctx->band_type = LOAD_CURRENT_BANDS;
-
-    task = g_task_new (self, NULL, callback, user_data);
-    g_task_set_task_data (task, ctx, (GDestroyNotify)mm_shared_telit_load_bands_context_free);
-
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               "#BND?",
                               3,
                               FALSE,
-                              (GAsyncReadyCallback) mm_shared_telit_load_bands_ready,
-                              task);
+                              (GAsyncReadyCallback) mm_shared_telit_load_current_bands_ready,
+                              g_task_new (self, NULL, callback, user_data));
 }
 
 /*****************************************************************************/
 /* Set current bands (Modem interface) */
 
 gboolean
-mm_shared_telit_modem_set_current_bands_finish (MMIfaceModem *self,
-                                                GAsyncResult *res,
-                                                GError **error)
+mm_shared_telit_modem_set_current_bands_finish (MMIfaceModem  *self,
+                                                GAsyncResult  *res,
+                                                GError       **error)
 {
     return g_task_propagate_boolean (G_TASK (res), error);
 }
 
 static void
-mm_shared_telit_modem_set_current_bands_ready (MMBaseModem *self,
-                                               GAsyncResult *res,
-                                               GTask *task)
+set_current_bands_ready (MMBaseModem  *self,
+                         GAsyncResult *res,
+                         GTask        *task)
 {
     GError *error = NULL;
 
@@ -247,88 +307,47 @@
 }
 
 void
-mm_shared_telit_modem_set_current_bands (MMIfaceModem *self,
-                                         GArray *bands_array,
-                                         GAsyncReadyCallback callback,
-                                         gpointer user_data)
+mm_shared_telit_modem_set_current_bands (MMIfaceModem        *self,
+                                         GArray              *bands_array,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
 {
-    gchar *cmd;
-    gint flag2g;
-    gint flag3g;
-    gint flag4g;
-    gboolean is_2g;
-    gboolean is_3g;
-    gboolean is_4g;
-    GTask *task;
+    GTask   *task;
+    GError  *error = NULL;
+    gchar   *cmd;
+    Private *priv;
 
-    mm_telit_get_band_flag (bands_array, &flag2g, &flag3g, &flag4g);
+    priv = get_private (MM_SHARED_TELIT (self));
 
-    is_2g = mm_iface_modem_is_2g (self);
-    is_3g = mm_iface_modem_is_3g (self);
-    is_4g = mm_iface_modem_is_4g (self);
-
-    if (is_2g && flag2g == -1) {
-        g_task_report_new_error (self,
-                                 callback,
-                                 user_data,
-                                 mm_shared_telit_modem_set_current_bands,
-                                 MM_CORE_ERROR,
-                                 MM_CORE_ERROR_NOT_FOUND,
-                                 "None or invalid 2G bands combination in the provided list");
-        return;
-    }
-
-    if (is_3g && flag3g == -1) {
-        g_task_report_new_error (self,
-                                 callback,
-                                 user_data,
-                                 mm_shared_telit_modem_set_current_bands,
-                                 MM_CORE_ERROR,
-                                 MM_CORE_ERROR_NOT_FOUND,
-                                 "None or invalid 3G bands combination in the provided list");
-        return;
-    }
-
-    if (is_4g && flag4g == -1) {
-        g_task_report_new_error (self,
-                                 callback,
-                                 user_data,
-                                 mm_shared_telit_modem_set_current_bands,
-                                 MM_CORE_ERROR,
-                                 MM_CORE_ERROR_NOT_FOUND,
-                                 "None or invalid 4G bands combination in the provided list");
-        return;
-    }
-
-    cmd = NULL;
-    if (is_2g && !is_3g && !is_4g)
-        cmd = g_strdup_printf ("AT#BND=%d", flag2g);
-    else if (is_2g && is_3g && !is_4g)
-        cmd = g_strdup_printf ("AT#BND=%d,%d", flag2g, flag3g);
-    else if (is_2g && is_3g && is_4g)
-        cmd = g_strdup_printf ("AT#BND=%d,%d,%d", flag2g, flag3g, flag4g);
-    else if (!is_2g && !is_3g && is_4g)
-        cmd = g_strdup_printf ("AT#BND=0,0,%d", flag4g);
-    else if (!is_2g && is_3g && is_4g)
-        cmd = g_strdup_printf ("AT#BND=0,%d,%d", flag3g, flag4g);
-    else if (is_2g && !is_3g && is_4g)
-        cmd = g_strdup_printf ("AT#BND=%d,0,%d", flag2g, flag4g);
-    else {
-        g_task_report_new_error (self,
-                                 callback,
-                                 user_data,
-                                 mm_shared_telit_modem_set_current_bands,
-                                 MM_CORE_ERROR,
-                                 MM_CORE_ERROR_FAILED,
-                                 "Unexpected error: could not compose AT#BND command");
-        return;
-    }
     task = g_task_new (self, NULL, callback, user_data);
+
+    if (bands_array->len == 1 && g_array_index (bands_array, MMModemBand, 0) == MM_MODEM_BAND_ANY) {
+        if (!priv->supported_bands) {
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "Couldn't build ANY band settings: unknown supported bands");
+            g_object_unref (task);
+            return;
+        }
+        bands_array = priv->supported_bands;
+    }
+
+    cmd = mm_telit_build_bnd_request (bands_array,
+                                      mm_iface_modem_is_2g (self),
+                                      mm_iface_modem_is_3g (self),
+                                      mm_iface_modem_is_4g (self),
+                                      priv->alternate_3g_bands,
+                                      &error);
+    if (!cmd) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               cmd,
                               20,
                               FALSE,
-                              (GAsyncReadyCallback)mm_shared_telit_modem_set_current_bands_ready,
+                              (GAsyncReadyCallback)set_current_bands_ready,
                               task);
     g_free (cmd);
 }
diff --git a/plugins/telit/mm-shared-telit.h b/plugins/telit/mm-shared-telit.h
index 2eb6d51..4cfaeb5 100644
--- a/plugins/telit/mm-shared-telit.h
+++ b/plugins/telit/mm-shared-telit.h
@@ -66,14 +66,18 @@
                                                          GAsyncReadyCallback callback,
                                                          gpointer user_data);
 
-GArray *    mm_shared_telit_modem_load_bands_finish     (MMIfaceModem *self,
-                                                         GAsyncResult *res,
-                                                         GError **error);
+GArray *    mm_shared_telit_modem_load_supported_bands_finish (MMIfaceModem *self,
+                                                               GAsyncResult *res,
+                                                               GError **error);
 
 void        mm_shared_telit_modem_load_current_bands    (MMIfaceModem *self,
                                                          GAsyncReadyCallback callback,
                                                          gpointer user_data);
 
+GArray *    mm_shared_telit_modem_load_current_bands_finish (MMIfaceModem *self,
+                                                             GAsyncResult *res,
+                                                             GError **error);
+
 gboolean    mm_shared_telit_modem_set_current_bands_finish (MMIfaceModem *self,
                                                             GAsyncResult *res,
                                                             GError **error);
diff --git a/plugins/telit/tests/test-mm-modem-helpers-telit.c b/plugins/telit/tests/test-mm-modem-helpers-telit.c
index 6103a2c..f95a97d 100644
--- a/plugins/telit/tests/test-mm-modem-helpers-telit.c
+++ b/plugins/telit/tests/test-mm-modem-helpers-telit.c
@@ -10,7 +10,8 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2015 Telit
+ * Copyright (C) 2015-2019 Telit
+ * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es>
  *
  */
 #include <stdio.h>
@@ -22,429 +23,489 @@
 #define _LIBMM_INSIDE_MM
 #include <libmm-glib.h>
 
+#include "mm-log.h"
 #include "mm-modem-helpers.h"
 #include "mm-modem-helpers-telit.h"
 
-static void
-test_mm_bands_contains (void) {
-    GArray* mm_bands;
-    guint i = 1;
+#include "test-helpers.h"
 
-    mm_bands = g_array_sized_new (FALSE, TRUE, sizeof (MMModemBand), 3);
+/******************************************************************************/
 
-    for (i = 0; i < 3; i++)
-        g_array_append_val (mm_bands, i);
-
-    g_assert (mm_telit_bands_contains (mm_bands, 2));
-    g_assert (mm_telit_bands_contains (mm_bands, 2));
-    g_assert (!mm_telit_bands_contains (mm_bands, 3));
-
-    g_array_free (mm_bands, TRUE);
-}
+#define MAX_BANDS_LIST_LEN 17
 
 typedef struct {
-    gchar* band_flag_str;
-    guint band_flags_len;
-    guint band_flags [MAX_BANDS_LIST_LEN];
-} BNDFlagsTest;
+    gchar       *response;
+    gboolean     modem_is_2g;
+    gboolean     modem_is_3g;
+    gboolean     modem_is_4g;
+    gboolean     modem_alternate_3g_bands;
+    guint        mm_bands_len;
+    MMModemBand  mm_bands [MAX_BANDS_LIST_LEN];
+} BndResponseTest;
 
-static BNDFlagsTest band_flag_test[] = {
-    {"0-3", 4, {0, 1, 2, 3} },
-    {"0,3", 2, {0, 3} },
-    {"0,2-3,5-7,9", 7, {0, 2, 3, 5, 6, 7, 9} },
-    { NULL, 0, {}},
+static BndResponseTest supported_band_mapping_tests [] = {
+    {
+        "#BND: (0-3)", TRUE, FALSE, FALSE, FALSE, 4,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850 }
+    },
+    {
+        "#BND: (0-3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, 7,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    {
+        "#BND: (0,3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, 7,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    {
+        "#BND: (0,2),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, 6,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    {
+        "#BND: (0,2),(0-4,5,6)", TRUE, TRUE, FALSE, FALSE, 7,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_2,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    {
+        "#BND: (0-3),(0,2,5,6),(1-1)", TRUE, TRUE, TRUE, FALSE, 8,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8,
+          MM_MODEM_BAND_EUTRAN_1 }
+    },
+    {
+        "#BND: (0),(0),(1-3)", TRUE, TRUE, TRUE, FALSE, 5,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2 }
+    },
+    {
+        "#BND: (0),(0),(1-3)", FALSE, FALSE, TRUE, FALSE, 2,
+        { MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2 }
+    },
+    /* 3G alternate band settings: default */
+    {
+        "#BND: (0),(0,2,5,6,12,25)", FALSE, TRUE, FALSE, FALSE, 5,
+        { MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8,
+          MM_MODEM_BAND_UTRAN_6,
+          MM_MODEM_BAND_UTRAN_19 }
+    },
+    /* 3G alternate band settings: alternate */
+    {
+        "#BND: (0),(0,2,5,6,12,13)", FALSE, TRUE, FALSE, TRUE, 4,
+        { MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_3,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    /* ME910 (2G+4G device)
+     * 168695967: 0xA0E189F: 0000 1010 0000 1110 0001 1000 1001 1111
+     */
+    {
+        "#BND: (0-5),(0),(1-168695967)", TRUE, FALSE, TRUE, FALSE, 17,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2,
+          MM_MODEM_BAND_EUTRAN_3,
+          MM_MODEM_BAND_EUTRAN_4,
+          MM_MODEM_BAND_EUTRAN_5,
+          MM_MODEM_BAND_EUTRAN_8,
+          MM_MODEM_BAND_EUTRAN_12,
+          MM_MODEM_BAND_EUTRAN_13,
+          MM_MODEM_BAND_EUTRAN_18,
+          MM_MODEM_BAND_EUTRAN_19,
+          MM_MODEM_BAND_EUTRAN_20,
+          MM_MODEM_BAND_EUTRAN_26,
+          MM_MODEM_BAND_EUTRAN_28 }
+    }
 };
 
 static void
-test_parse_band_flag_str (void) {
-    GError *error = NULL;
-    gboolean res = FALSE;
-    GArray *band_flags = NULL;
-    guint i, j;
+test_parse_supported_bands_response (void)
+{
+    guint i;
 
-    for (i = 0; band_flag_test[i].band_flag_str != NULL; i++) {
-        band_flags = g_array_new (FALSE, FALSE, sizeof (guint));
-        res = mm_telit_get_band_flags_from_string (band_flag_test[i].band_flag_str,
-                                                   &band_flags,
+    for (i = 0; i < G_N_ELEMENTS (supported_band_mapping_tests); i++) {
+        GError *error = NULL;
+        GArray *bands = NULL;
+
+        bands = mm_telit_parse_bnd_test_response (supported_band_mapping_tests[i].response,
+                                                  supported_band_mapping_tests[i].modem_is_2g,
+                                                  supported_band_mapping_tests[i].modem_is_3g,
+                                                  supported_band_mapping_tests[i].modem_is_4g,
+                                                  supported_band_mapping_tests[i].modem_alternate_3g_bands,
+                                                  &error);
+        g_assert_no_error (error);
+        g_assert (bands);
+
+        mm_test_helpers_compare_bands (bands,
+                                       supported_band_mapping_tests[i].mm_bands,
+                                       supported_band_mapping_tests[i].mm_bands_len);
+        g_array_unref (bands);
+    }
+}
+
+static BndResponseTest current_band_mapping_tests [] = {
+    {
+        "#BND: 0", TRUE, FALSE, FALSE, FALSE, 2,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS }
+    },
+    {
+        "#BND: 0,5", TRUE, TRUE, FALSE, FALSE, 3,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    {
+        "#BND: 1,3", TRUE, TRUE, FALSE, FALSE, 5,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_2,
+          MM_MODEM_BAND_UTRAN_5 }
+    },
+    {
+        "#BND: 2,7", TRUE, TRUE, FALSE, FALSE, 3,
+        { MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_4 }
+    },
+    {
+        "#BND: 3,0,1", TRUE, TRUE, TRUE, FALSE, 4,
+        { MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_EUTRAN_1 }
+    },
+    {
+        "#BND: 0,0,3", TRUE, FALSE, TRUE, FALSE, 4,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2 }
+    },
+    {
+        "#BND: 0,0,3", FALSE, FALSE, TRUE, FALSE, 2,
+        { MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2 }
+    },
+    /* 3G alternate band settings: default */
+    {
+        "#BND: 0,12", FALSE, TRUE, FALSE, FALSE, 1,
+        { MM_MODEM_BAND_UTRAN_6 }
+    },
+    /* 3G alternate band settings: alternate */
+    {
+        "#BND: 0,12", FALSE, TRUE, FALSE, TRUE, 4,
+        { MM_MODEM_BAND_UTRAN_1,
+          MM_MODEM_BAND_UTRAN_3,
+          MM_MODEM_BAND_UTRAN_5,
+          MM_MODEM_BAND_UTRAN_8 }
+    },
+    /* ME910 (2G+4G device)
+     * 168695967: 0xA0E189F: 0000 1010 0000 1110 0001 1000 1001 1111
+     */
+    {
+        "#BND: 5,0,168695967", TRUE, FALSE, TRUE, FALSE, 17,
+        { MM_MODEM_BAND_EGSM,
+          MM_MODEM_BAND_DCS,
+          MM_MODEM_BAND_PCS,
+          MM_MODEM_BAND_G850,
+          MM_MODEM_BAND_EUTRAN_1,
+          MM_MODEM_BAND_EUTRAN_2,
+          MM_MODEM_BAND_EUTRAN_3,
+          MM_MODEM_BAND_EUTRAN_4,
+          MM_MODEM_BAND_EUTRAN_5,
+          MM_MODEM_BAND_EUTRAN_8,
+          MM_MODEM_BAND_EUTRAN_12,
+          MM_MODEM_BAND_EUTRAN_13,
+          MM_MODEM_BAND_EUTRAN_18,
+          MM_MODEM_BAND_EUTRAN_19,
+          MM_MODEM_BAND_EUTRAN_20,
+          MM_MODEM_BAND_EUTRAN_26,
+          MM_MODEM_BAND_EUTRAN_28 }
+    }
+};
+
+static void
+test_parse_current_bands_response (void)
+{
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS (current_band_mapping_tests); i++) {
+        GError *error = NULL;
+        GArray *bands = NULL;
+
+        bands = mm_telit_parse_bnd_query_response (current_band_mapping_tests[i].response,
+                                                   current_band_mapping_tests[i].modem_is_2g,
+                                                   current_band_mapping_tests[i].modem_is_3g,
+                                                   current_band_mapping_tests[i].modem_is_4g,
+                                                   current_band_mapping_tests[i].modem_alternate_3g_bands,
                                                    &error);
         g_assert_no_error (error);
-        g_assert (res);
+        g_assert (bands);
 
-        for (j = 0; j < band_flag_test[i].band_flags_len; j++) {
-            guint ref;
-            guint cur;
-
-            ref = band_flag_test[i].band_flags[j];
-            cur = g_array_index (band_flags, guint, j);
-
-            g_assert (ref == cur);
-        }
-
-        g_array_free (band_flags, TRUE);
+        mm_test_helpers_compare_bands (bands,
+                                       current_band_mapping_tests[i].mm_bands,
+                                       current_band_mapping_tests[i].mm_bands_len);
+        g_array_unref (bands);
     }
 }
 
-typedef struct {
-    gchar* response;
-    gboolean modem_is_2g;
-    gboolean modem_is_3g;
-    gboolean modem_is_4g;
-    guint mm_bands_len;
-    MMModemBand mm_bands [MAX_BANDS_LIST_LEN];
-} BNDResponseTest;
-
-static BNDResponseTest supported_band_mapping_tests [] = {
-    { "#BND: (0-3)", TRUE, FALSE, FALSE, 4, { MM_MODEM_BAND_EGSM,
-                                              MM_MODEM_BAND_DCS,
-                                              MM_MODEM_BAND_PCS,
-                                              MM_MODEM_BAND_G850} },
-    { "#BND: (0-3),(0,2,5,6)", TRUE, TRUE, FALSE, 7, { MM_MODEM_BAND_EGSM,
-                                                      MM_MODEM_BAND_DCS,
-                                                      MM_MODEM_BAND_PCS,
-                                                      MM_MODEM_BAND_G850,
-                                                      MM_MODEM_BAND_UTRAN_1,
-                                                      MM_MODEM_BAND_UTRAN_5,
-                                                      MM_MODEM_BAND_UTRAN_8 } },
-    { "#BND: (0,3),(0,2,5,6)", TRUE, TRUE, FALSE, 7, { MM_MODEM_BAND_EGSM,
-                                                       MM_MODEM_BAND_DCS,
-                                                       MM_MODEM_BAND_PCS,
-                                                       MM_MODEM_BAND_G850,
-                                                       MM_MODEM_BAND_UTRAN_1,
-                                                       MM_MODEM_BAND_UTRAN_5,
-                                                       MM_MODEM_BAND_UTRAN_8} },
-    { "#BND: (0,2),(0,2,5,6)", TRUE, TRUE, FALSE, 6, { MM_MODEM_BAND_EGSM,
-                                                       MM_MODEM_BAND_DCS,
-                                                       MM_MODEM_BAND_G850,
-                                                       MM_MODEM_BAND_UTRAN_1,
-                                                       MM_MODEM_BAND_UTRAN_5,
-                                                       MM_MODEM_BAND_UTRAN_8} },
-    { "#BND: (0,2),(0-4,5,6)", TRUE, TRUE, FALSE, 7, { MM_MODEM_BAND_EGSM,
-                                                       MM_MODEM_BAND_DCS,
-                                                       MM_MODEM_BAND_G850,
-                                                       MM_MODEM_BAND_UTRAN_1,
-                                                       MM_MODEM_BAND_UTRAN_2,
-                                                       MM_MODEM_BAND_UTRAN_5,
-                                                       MM_MODEM_BAND_UTRAN_8} },
-    { "#BND: (0-3),(0,2,5,6),(1-1)", TRUE, TRUE, TRUE, 8, { MM_MODEM_BAND_EGSM,
-                                                         MM_MODEM_BAND_DCS,
-                                                         MM_MODEM_BAND_PCS,
-                                                         MM_MODEM_BAND_G850,
-                                                         MM_MODEM_BAND_UTRAN_1,
-                                                         MM_MODEM_BAND_UTRAN_5,
-                                                         MM_MODEM_BAND_UTRAN_8,
-                                                         MM_MODEM_BAND_EUTRAN_1} },
-    { "#BND: (0),(0),(1-3)", TRUE, TRUE, TRUE, 5, { MM_MODEM_BAND_EGSM,
-                                                    MM_MODEM_BAND_DCS,
-                                                    MM_MODEM_BAND_UTRAN_1,
-                                                    MM_MODEM_BAND_EUTRAN_1,
-                                                    MM_MODEM_BAND_EUTRAN_2} },
-    { "#BND: (0),(0),(1-3)", FALSE, FALSE, TRUE, 2, { MM_MODEM_BAND_EUTRAN_1,
-                                                      MM_MODEM_BAND_EUTRAN_2} },
-    { NULL, FALSE, FALSE, FALSE, 0, {}},
-};
+/******************************************************************************/
 
 static void
-test_parse_supported_bands_response (void) {
-    GError* error = NULL;
-    gboolean res = FALSE;
-    guint i, j;
-    GArray* bands = NULL;
+test_common_bnd_cmd (const gchar *expected_cmd,
+                     gboolean     modem_is_2g,
+                     gboolean     modem_is_3g,
+                     gboolean     modem_is_4g,
+                     gboolean     modem_alternate_3g_bands,
+                     GArray      *bands_array)
+{
+    gchar  *cmd;
+    GError *error = NULL;
 
-    for (i = 0; supported_band_mapping_tests[i].response != NULL; i++) {
-        res = mm_telit_parse_bnd_response (supported_band_mapping_tests[i].response,
-                                           supported_band_mapping_tests[i].modem_is_2g,
-                                           supported_band_mapping_tests[i].modem_is_3g,
-                                           supported_band_mapping_tests[i].modem_is_4g,
-                                           LOAD_SUPPORTED_BANDS,
-                                           &bands,
-                                           &error);
-        g_assert_no_error (error);
-        g_assert (res);
-
-
-        for (j = 0; j < supported_band_mapping_tests[i].mm_bands_len; j++) {
-            MMModemBand ref;
-            MMModemBand cur;
-
-            ref = supported_band_mapping_tests[i].mm_bands[j];
-            cur = g_array_index (bands, MMModemBand, j);
-            g_assert_cmpint (cur, ==, ref);
-        }
-
-        g_assert_cmpint (bands->len, ==, supported_band_mapping_tests[i].mm_bands_len);
-
-        g_array_free (bands, TRUE);
-        bands = NULL;
-    }
+    cmd = mm_telit_build_bnd_request (bands_array,
+                                      modem_is_2g, modem_is_3g, modem_is_4g,
+                                      modem_alternate_3g_bands,
+                                      &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (cmd, ==, expected_cmd);
+    g_free (cmd);
 }
 
-
-static BNDResponseTest current_band_mapping_tests [] = {
-    { "#BND: 0", TRUE, FALSE, FALSE, 2, { MM_MODEM_BAND_EGSM,
-                                          MM_MODEM_BAND_DCS
-                                        }
-    },
-    { "#BND: 0,5", TRUE, TRUE, FALSE, 3, { MM_MODEM_BAND_EGSM,
-                                           MM_MODEM_BAND_DCS,
-                                           MM_MODEM_BAND_UTRAN_8
-                                         }
-    },
-    { "#BND: 1,3", TRUE, TRUE, FALSE, 5, { MM_MODEM_BAND_EGSM,
-                                           MM_MODEM_BAND_PCS,
-                                           MM_MODEM_BAND_UTRAN_1,
-                                           MM_MODEM_BAND_UTRAN_2,
-                                           MM_MODEM_BAND_UTRAN_5,
-                                         }
-    },
-    { "#BND: 2,7", TRUE, TRUE, FALSE, 3, { MM_MODEM_BAND_DCS,
-                                           MM_MODEM_BAND_G850,
-                                           MM_MODEM_BAND_UTRAN_4
-                                         }
-    },
-    { "#BND: 3,0,1", TRUE, TRUE, TRUE, 4, { MM_MODEM_BAND_PCS,
-                                            MM_MODEM_BAND_G850,
-                                            MM_MODEM_BAND_UTRAN_1,
-                                            MM_MODEM_BAND_EUTRAN_1
-                                          }
-    },
-    { "#BND: 0,0,3", TRUE, FALSE, TRUE, 4, { MM_MODEM_BAND_EGSM,
-                                             MM_MODEM_BAND_DCS,
-                                             MM_MODEM_BAND_EUTRAN_1,
-                                             MM_MODEM_BAND_EUTRAN_2
-                                           }
-    },
-    { "#BND: 0,0,3", FALSE, FALSE, TRUE, 2, { MM_MODEM_BAND_EUTRAN_1,
-                                              MM_MODEM_BAND_EUTRAN_2
-                                            }
-    },
-    { NULL, FALSE, FALSE, FALSE, 0, {}},
-};
+#define test_common_bnd_cmd_2g(EXPECTED_CMD, BANDS_ARRAY) test_common_bnd_cmd (EXPECTED_CMD, TRUE, FALSE, FALSE, FALSE, BANDS_ARRAY)
+#define test_common_bnd_cmd_3g(EXPECTED_CMD, ALTERNATE, BANDS_ARRAY) test_common_bnd_cmd (EXPECTED_CMD, FALSE, TRUE, FALSE, ALTERNATE, BANDS_ARRAY)
+#define test_common_bnd_cmd_4g(EXPECTED_CMD, BANDS_ARRAY) test_common_bnd_cmd (EXPECTED_CMD, FALSE, FALSE, TRUE, FALSE, BANDS_ARRAY)
 
 static void
-test_parse_current_bands_response (void) {
-    GError* error = NULL;
-    gboolean res = FALSE;
-    guint i, j;
-    GArray* bands = NULL;
+test_common_bnd_cmd_invalid (gboolean  modem_is_2g,
+                             gboolean  modem_is_3g,
+                             gboolean  modem_is_4g,
+                             GArray   *bands_array)
+{
+    gchar  *cmd;
+    GError *error = NULL;
 
-    for (i = 0; current_band_mapping_tests[i].response != NULL; i++) {
-        res = mm_telit_parse_bnd_response (current_band_mapping_tests[i].response,
-                                           current_band_mapping_tests[i].modem_is_2g,
-                                           current_band_mapping_tests[i].modem_is_3g,
-                                           current_band_mapping_tests[i].modem_is_4g,
-                                           LOAD_CURRENT_BANDS,
-                                           &bands,
-                                           &error);
-        g_assert_no_error (error);
-        g_assert (res);
-
-
-        for (j = 0; j < current_band_mapping_tests[i].mm_bands_len; j++) {
-            MMModemBand ref;
-            MMModemBand cur;
-
-            ref = current_band_mapping_tests[i].mm_bands[j];
-            cur = g_array_index (bands, MMModemBand, j);
-            g_assert_cmpint (cur, ==, ref);
-        }
-
-        g_assert_cmpint (bands->len, ==, current_band_mapping_tests[i].mm_bands_len);
-
-        g_array_free (bands, TRUE);
-        bands = NULL;
-    }
+    cmd = mm_telit_build_bnd_request (bands_array,
+                                      modem_is_2g, modem_is_3g, modem_is_4g,
+                                      FALSE,
+                                      &error);
+    g_assert_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED);
+    g_assert (!cmd);
 }
 
+#define test_common_bnd_cmd_2g_invalid(BANDS_ARRAY) test_common_bnd_cmd_invalid (TRUE, FALSE, FALSE, BANDS_ARRAY)
+#define test_common_bnd_cmd_3g_invalid(BANDS_ARRAY) test_common_bnd_cmd_invalid (FALSE, TRUE, FALSE, BANDS_ARRAY)
+#define test_common_bnd_cmd_4g_invalid(BANDS_ARRAY) test_common_bnd_cmd_invalid (FALSE, FALSE, TRUE, BANDS_ARRAY)
+
 static void
 test_telit_get_2g_bnd_flag (void)
 {
-    GArray *bands_array;
-    gint flag2g;
-    MMModemBand egsm = MM_MODEM_BAND_EGSM;
-    MMModemBand dcs = MM_MODEM_BAND_DCS;
-    MMModemBand pcs = MM_MODEM_BAND_PCS;
-    MMModemBand g850 = MM_MODEM_BAND_G850;
+    GArray      *bands_array;
+    MMModemBand  egsm = MM_MODEM_BAND_EGSM;
+    MMModemBand  dcs  = MM_MODEM_BAND_DCS;
+    MMModemBand  pcs  = MM_MODEM_BAND_PCS;
+    MMModemBand  g850 = MM_MODEM_BAND_G850;
 
     /* Test Flag 0 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, egsm);
     g_array_append_val (bands_array, dcs);
-
-    mm_telit_get_band_flag (bands_array, &flag2g, NULL, NULL);
-    g_assert_cmpuint (flag2g, ==, 0);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_2g ("#BND=0", bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 1 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, egsm);
     g_array_append_val (bands_array, pcs);
-
-    mm_telit_get_band_flag (bands_array, &flag2g, NULL, NULL);
-    g_assert_cmpuint (flag2g, ==, 1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_2g ("#BND=1", bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 2 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, g850);
     g_array_append_val (bands_array, dcs);
-
-    mm_telit_get_band_flag (bands_array, &flag2g, NULL, NULL);
-    g_assert_cmpuint (flag2g, ==, 2);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_2g ("#BND=2", bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 3 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, g850);
     g_array_append_val (bands_array, pcs);
-
-    mm_telit_get_band_flag (bands_array, &flag2g, NULL, NULL);
-    g_assert_cmpuint (flag2g, ==, 3);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_2g ("#BND=3", bands_array);
+    g_array_unref (bands_array);
 
     /* Test invalid band array */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, g850);
     g_array_append_val (bands_array, egsm);
-
-    mm_telit_get_band_flag (bands_array, &flag2g, NULL, NULL);
-    g_assert_cmpuint (flag2g, ==, -1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_2g_invalid (bands_array);
+    g_array_unref (bands_array);
 }
 
-
 static void
 test_telit_get_3g_bnd_flag (void)
 {
-    GArray *bands_array;
-    MMModemBand u2100 = MM_MODEM_BAND_UTRAN_1;
-    MMModemBand u1900 = MM_MODEM_BAND_UTRAN_2;
-    MMModemBand u850 = MM_MODEM_BAND_UTRAN_5;
-    MMModemBand u900 = MM_MODEM_BAND_UTRAN_8;
-    MMModemBand u17iv = MM_MODEM_BAND_UTRAN_4;
-    MMModemBand u17ix = MM_MODEM_BAND_UTRAN_9;
-    gint flag;
+    GArray      *bands_array;
+    MMModemBand  u2100 = MM_MODEM_BAND_UTRAN_1;
+    MMModemBand  u1900 = MM_MODEM_BAND_UTRAN_2;
+    MMModemBand  u1800 = MM_MODEM_BAND_UTRAN_3;
+    MMModemBand  u850  = MM_MODEM_BAND_UTRAN_5;
+    MMModemBand  u800  = MM_MODEM_BAND_UTRAN_6;
+    MMModemBand  u900  = MM_MODEM_BAND_UTRAN_8;
+    MMModemBand  u17iv = MM_MODEM_BAND_UTRAN_4;
+    MMModemBand  u17ix = MM_MODEM_BAND_UTRAN_9;
 
     /* Test flag 0 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u2100);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 0);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,0", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,0", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 1 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u1900);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,1", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,1", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 2 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u850);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 2);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,2", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,2", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 3 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 3);
     g_array_append_val (bands_array, u2100);
     g_array_append_val (bands_array, u1900);
     g_array_append_val (bands_array, u850);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 3);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,3", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,3", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 4 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, u1900);
     g_array_append_val (bands_array, u850);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 4);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,4", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,4", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 5 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u900);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 5);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,5", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,5", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 6 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, u2100);
     g_array_append_val (bands_array, u900);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 6);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g ("#BND=0,6", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,6", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 7 */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u17iv);
+    test_common_bnd_cmd_3g ("#BND=0,7", FALSE, bands_array);
+    test_common_bnd_cmd_3g ("#BND=0,7", TRUE,  bands_array);
+    g_array_unref (bands_array);
 
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, 7);
-    g_array_free (bands_array, TRUE);
+    /* Test flag 12 in default */
+    bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
+    g_array_append_val (bands_array, u800);
+    test_common_bnd_cmd_3g ("#BND=0,12", FALSE, bands_array);
+    g_array_unref (bands_array);
+
+    /* Test flag 12 in alternate */
+    bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 4);
+    g_array_append_val (bands_array, u2100);
+    g_array_append_val (bands_array, u1800);
+    g_array_append_val (bands_array, u850);
+    g_array_append_val (bands_array, u900);
+    test_common_bnd_cmd_3g ("#BND=0,12", TRUE, bands_array);
+    g_array_unref (bands_array);
 
     /* Test invalid band array */
-    flag = -1;
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, u17ix);
-
-    mm_telit_get_band_flag (bands_array, NULL, &flag, NULL);
-    g_assert_cmpint (flag, ==, -1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_3g_invalid (bands_array);
+    g_array_unref (bands_array);
 }
 
 static void
 test_telit_get_4g_bnd_flag (void)
 {
-    GArray *bands_array;
-    MMModemBand eutran_i = MM_MODEM_BAND_EUTRAN_1;
-    MMModemBand eutran_ii = MM_MODEM_BAND_EUTRAN_2;
-    MMModemBand egsm = MM_MODEM_BAND_EGSM;
-    gint flag = -1;
+    GArray      *bands_array;
+    MMModemBand  eutran_i = MM_MODEM_BAND_EUTRAN_1;
+    MMModemBand  eutran_ii = MM_MODEM_BAND_EUTRAN_2;
+    MMModemBand  egsm = MM_MODEM_BAND_EGSM;
 
     /* Test flag 1 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, eutran_i);
-
-    mm_telit_get_band_flag (bands_array, NULL, NULL, &flag);
-    g_assert_cmpint (flag, ==, 1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_4g ("#BND=0,0,1", bands_array);
+    g_array_unref (bands_array);
 
     /* Test flag 3 */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 2);
     g_array_append_val (bands_array, eutran_i);
     g_array_append_val (bands_array, eutran_ii);
-
-    mm_telit_get_band_flag (bands_array, NULL, NULL, &flag);
-    g_assert_cmpint (flag, ==, 3);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_4g ("#BND=0,0,3", bands_array);
+    g_array_unref (bands_array);
 
     /* Test invalid bands array */
     bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), 1);
     g_array_append_val (bands_array, egsm);
-
-    mm_telit_get_band_flag (bands_array, NULL, NULL, &flag);
-    g_assert_cmpint (flag, ==, -1);
-    g_array_free (bands_array, TRUE);
+    test_common_bnd_cmd_4g_invalid (bands_array);
+    g_array_unref (bands_array);
 }
 
+/******************************************************************************/
+
 typedef struct {
     const char* response;
     MMTelitQssStatus expected_qss;
@@ -483,14 +544,34 @@
     }
 }
 
+/******************************************************************************/
+
+void
+_mm_log (const char *loc,
+         const char *func,
+         guint32 level,
+         const char *fmt,
+         ...)
+{
+#if defined ENABLE_TEST_MESSAGE_TRACES
+    /* Dummy log function */
+    va_list args;
+    gchar *msg;
+
+    va_start (args, fmt);
+    msg = g_strdup_vprintf (fmt, args);
+    va_end (args);
+    g_print ("%s\n", msg);
+    g_free (msg);
+#endif
+}
+
 int main (int argc, char **argv)
 {
     setlocale (LC_ALL, "");
 
     g_test_init (&argc, &argv, NULL);
 
-    g_test_add_func ("/MM/telit/bands/supported/bands_contains", test_mm_bands_contains);
-    g_test_add_func ("/MM/telit/bands/supported/parse_band_flag", test_parse_band_flag_str);
     g_test_add_func ("/MM/telit/bands/supported/parse_bands_response", test_parse_supported_bands_response);
     g_test_add_func ("/MM/telit/bands/current/parse_bands_response", test_parse_current_bands_response);
     g_test_add_func ("/MM/telit/bands/current/set_bands/2g", test_telit_get_2g_bnd_flag);
diff --git a/plugins/tests/test-helpers.c b/plugins/tests/test-helpers.c
new file mode 100644
index 0000000..5620c04
--- /dev/null
+++ b/plugins/tests/test-helpers.c
@@ -0,0 +1,52 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <ModemManager.h>
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+#include "mm-log.h"
+#include "mm-modem-helpers.h"
+
+#include "test-helpers.h"
+
+void
+mm_test_helpers_compare_bands (GArray            *bands,
+			       const MMModemBand *expected_bands,
+			       guint              n_expected_bands)
+{
+    gchar  *bands_str;
+    GArray *expected_bands_array;
+    gchar  *expected_bands_str;
+
+    if (!expected_bands || !n_expected_bands) {
+        g_assert (!bands);
+        return;
+    }
+
+    g_assert (bands);
+    mm_common_bands_garray_sort (bands);
+    bands_str = mm_common_build_bands_string ((MMModemBand *)(bands->data), bands->len);
+
+    expected_bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), n_expected_bands);
+    g_array_append_vals (expected_bands_array, expected_bands, n_expected_bands);
+    mm_common_bands_garray_sort (expected_bands_array);
+    expected_bands_str = mm_common_build_bands_string ((MMModemBand *)(expected_bands_array->data), expected_bands_array->len);
+    g_array_unref (expected_bands_array);
+
+    g_assert_cmpstr (bands_str, ==, expected_bands_str);
+    g_free (bands_str);
+    g_free (expected_bands_str);
+}
diff --git a/plugins/tests/test-helpers.h b/plugins/tests/test-helpers.h
new file mode 100644
index 0000000..8362754
--- /dev/null
+++ b/plugins/tests/test-helpers.h
@@ -0,0 +1,26 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+#include <glib.h>
+#include <libmm-glib.h>
+
+void mm_test_helpers_compare_bands (GArray            *bands,
+                                    const MMModemBand *expected_bands,
+                                    guint              n_expected_bands);
+
+#endif /* TEST_HELPERS_H */
diff --git a/plugins/thuraya/mm-modem-helpers-thuraya.c b/plugins/thuraya/mm-modem-helpers-thuraya.c
index b940e63..1dad59f 100644
--- a/plugins/thuraya/mm-modem-helpers-thuraya.c
+++ b/plugins/thuraya/mm-modem-helpers-thuraya.c
@@ -99,7 +99,7 @@
         array = g_array_new (FALSE, FALSE, sizeof (MMSmsStorage));
 
         /* Got a range group to match */
-        if (g_regex_match_full (r, splita[i], strlen (splita[i]), 0, 0, &match_info, NULL)) {
+        if (g_regex_match (r, splita[i], 0, &match_info)) {
             while (g_match_info_matches (match_info)) {
                 gchar *str;
 
diff --git a/plugins/tplink/77-mm-tplink-port-types.rules b/plugins/tplink/77-mm-tplink-port-types.rules
new file mode 100644
index 0000000..34c9638
--- /dev/null
+++ b/plugins/tplink/77-mm-tplink-port-types.rules
@@ -0,0 +1,15 @@
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move|bind", GOTO="mm_tplink_port_types_end"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="2357", GOTO="mm_tplink_port_types"
+GOTO="mm_tplink_port_types_end"
+
+LABEL="mm_tplink_port_types"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# D-Link DWM-222
+ATTRS{idVendor}=="2357", ATTRS{idProduct}=="9000", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="2357", ATTRS{idProduct}=="9000", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_PORT_IGNORE}="1"
+ATTRS{idVendor}=="2357", ATTRS{idProduct}=="9000", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+
+LABEL="mm_tplink_port_types_end"
diff --git a/plugins/tplink/mm-plugin-tplink.c b/plugins/tplink/mm-plugin-tplink.c
new file mode 100644
index 0000000..20cf9db
--- /dev/null
+++ b/plugins/tplink/mm-plugin-tplink.c
@@ -0,0 +1,94 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <string.h>
+#include <gmodule.h>
+
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+#include "mm-port-enums-types.h"
+#include "mm-log.h"
+#include "mm-plugin-tplink.h"
+#include "mm-broadband-modem.h"
+
+#if defined WITH_QMI
+# include "mm-broadband-modem-qmi.h"
+#endif
+
+G_DEFINE_TYPE (MMPluginTplink, mm_plugin_tplink, MM_TYPE_PLUGIN)
+
+MM_PLUGIN_DEFINE_MAJOR_VERSION
+MM_PLUGIN_DEFINE_MINOR_VERSION
+
+/*****************************************************************************/
+
+static MMBaseModem *
+create_modem (MMPlugin     *self,
+              const gchar  *uid,
+              const gchar **drivers,
+              guint16       vendor,
+              guint16       product,
+              GList        *probes,
+              GError      **error)
+{
+#if defined WITH_QMI
+    if (mm_port_probe_list_has_qmi_port (probes)) {
+        mm_dbg ("QMI-powered TP-Link modem found...");
+        return MM_BASE_MODEM (mm_broadband_modem_qmi_new (uid,
+                                                          drivers,
+                                                          mm_plugin_get_name (self),
+                                                          vendor,
+                                                          product));
+    }
+#endif
+
+    return MM_BASE_MODEM (mm_broadband_modem_new (uid,
+                                                  drivers,
+                                                  mm_plugin_get_name (self),
+                                                  vendor,
+                                                  product));
+}
+
+/*****************************************************************************/
+
+G_MODULE_EXPORT MMPlugin *
+mm_plugin_create (void)
+{
+    static const gchar *subsystems[] = { "tty", "net", "usb", NULL };
+    static const guint16 vendor_ids[] = { 0x2357, 0 };
+
+    return MM_PLUGIN (
+        g_object_new (MM_TYPE_PLUGIN_TPLINK,
+                      MM_PLUGIN_NAME,               "TP-Link",
+                      MM_PLUGIN_ALLOWED_SUBSYSTEMS, subsystems,
+                      MM_PLUGIN_ALLOWED_VENDOR_IDS, vendor_ids,
+                      MM_PLUGIN_ALLOWED_AT,         TRUE,
+                      MM_PLUGIN_ALLOWED_QMI,        TRUE,
+                      NULL));
+}
+
+static void
+mm_plugin_tplink_init (MMPluginTplink *self)
+{
+}
+
+static void
+mm_plugin_tplink_class_init (MMPluginTplinkClass *klass)
+{
+    MMPluginClass *plugin_class = MM_PLUGIN_CLASS (klass);
+
+    plugin_class->create_modem = create_modem;
+}
diff --git a/plugins/tplink/mm-plugin-tplink.h b/plugins/tplink/mm-plugin-tplink.h
new file mode 100644
index 0000000..16dc5f5
--- /dev/null
+++ b/plugins/tplink/mm-plugin-tplink.h
@@ -0,0 +1,40 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef MM_PLUGIN_TPLINK_H
+#define MM_PLUGIN_TPLINK_H
+
+#include "mm-plugin.h"
+
+#define MM_TYPE_PLUGIN_TPLINK            (mm_plugin_tplink_get_type ())
+#define MM_PLUGIN_TPLINK(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_PLUGIN_TPLINK, MMPluginTplink))
+#define MM_PLUGIN_TPLINK_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_PLUGIN_TPLINK, MMPluginTplinkClass))
+#define MM_IS_PLUGIN_TPLINK(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_PLUGIN_TPLINK))
+#define MM_IS_PLUGIN_TPLINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_PLUGIN_TPLINK))
+#define MM_PLUGIN_TPLINK_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_PLUGIN_TPLINK, MMPluginTplinkClass))
+
+typedef struct {
+    MMPlugin parent;
+} MMPluginTplink;
+
+typedef struct {
+    MMPluginClass parent;
+} MMPluginTplinkClass;
+
+GType mm_plugin_tplink_get_type (void);
+
+G_MODULE_EXPORT MMPlugin *mm_plugin_create (void);
+
+#endif /* MM_PLUGIN_TPLINK_H */
diff --git a/plugins/ublox/mm-broadband-modem-ublox.c b/plugins/ublox/mm-broadband-modem-ublox.c
index fe1c319..509c4c6 100644
--- a/plugins/ublox/mm-broadband-modem-ublox.c
+++ b/plugins/ublox/mm-broadband-modem-ublox.c
@@ -10,7 +10,7 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2016-2018 Aleksander Morgado <aleksander@aleksander.es>
+ * Copyright (C) 2016-2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include <config.h>
@@ -33,7 +33,6 @@
 #include "mm-sim-ublox.h"
 #include "mm-modem-helpers-ublox.h"
 #include "mm-ublox-enums-types.h"
-#include "mm-call-ublox.h"
 
 static void iface_modem_init (MMIfaceModem *iface);
 static void iface_modem_3gpp_init (MMIfaceModem3gpp *iface);
@@ -67,6 +66,9 @@
     /* Operator ID for manual registration */
     gchar *operator_id;
 
+    /* Voice +UCALLSTAT support */
+    GRegex *ucallstat_regex;
+
     /* Regex to ignore */
     GRegex *pbready_regex;
 };
@@ -1005,6 +1007,23 @@
 }
 
 static void
+parent_voice_disable_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
+{
+    GError *error = NULL;
+
+    if (!iface_modem_voice_parent->disable_unsolicited_events_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
 own_voice_disable_unsolicited_events_ready (MMBaseModem  *self,
                                             GAsyncResult *res,
                                             GTask        *task)
@@ -1012,11 +1031,17 @@
     GError *error = NULL;
 
     mm_base_modem_at_command_full_finish (self, res, &error);
-    if (error)
+    if (error) {
         g_task_return_error (task, error);
-    else
-        g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Chain up parent's disable */
+    iface_modem_voice_parent->disable_unsolicited_events (
+        MM_IFACE_MODEM_VOICE (self),
+        (GAsyncReadyCallback)parent_voice_disable_unsolicited_events_ready,
+        task);
 }
 
 static void
@@ -1028,8 +1053,6 @@
 
     task = g_task_new (self, NULL, callback, user_data);
 
-    /* Note: no parent disable method */
-
     mm_base_modem_at_command_full (
         MM_BASE_MODEM (self),
         mm_base_modem_peek_port_primary (MM_BASE_MODEM (self)),
@@ -1043,6 +1066,177 @@
 }
 
 /*****************************************************************************/
+/* Common setup/cleanup voice unsolicited events */
+
+static void
+ucallstat_received (MMPortSerialAt        *port,
+                    GMatchInfo            *match_info,
+                    MMBroadbandModemUblox *self)
+{
+    static const MMCallState ublox_call_state[] = {
+        [0] = MM_CALL_STATE_ACTIVE,
+        [1] = MM_CALL_STATE_HELD,
+        [2] = MM_CALL_STATE_DIALING,     /* Dialing  (MOC) */
+        [3] = MM_CALL_STATE_RINGING_OUT, /* Alerting (MOC) */
+        [4] = MM_CALL_STATE_RINGING_IN,  /* Incoming (MTC) */
+        [5] = MM_CALL_STATE_WAITING,     /* Waiting  (MTC) */
+        [6] = MM_CALL_STATE_TERMINATED,
+        [7] = MM_CALL_STATE_ACTIVE,      /* Treated same way as ACTIVE */
+    };
+
+    MMCallInfo call_info = { 0 };
+    guint      aux;
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &aux)) {
+        mm_warn ("couldn't parse call index from +UCALLSTAT");
+        return;
+    }
+    call_info.index = aux;
+
+    if (!mm_get_uint_from_match_info (match_info, 2, &aux) ||
+        (aux >= G_N_ELEMENTS (ublox_call_state))) {
+        mm_warn ("couldn't parse call state from +UCALLSTAT");
+        return;
+    }
+    call_info.state = ublox_call_state[aux];
+
+    /* guess direction for some of the states */
+    switch (call_info.state) {
+    case MM_CALL_STATE_DIALING:
+    case MM_CALL_STATE_RINGING_OUT:
+        call_info.direction = MM_CALL_DIRECTION_OUTGOING;
+        break;
+    case MM_CALL_STATE_RINGING_IN:
+    case MM_CALL_STATE_WAITING:
+        call_info.direction = MM_CALL_DIRECTION_INCOMING;
+        break;
+    default:
+        call_info.direction = MM_CALL_DIRECTION_UNKNOWN;
+        break;
+    }
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+common_voice_setup_cleanup_unsolicited_events (MMBroadbandModemUblox *self,
+                                               gboolean               enable)
+{
+    MMPortSerialAt *ports[2];
+    guint           i;
+
+    if (G_UNLIKELY (!self->priv->ucallstat_regex))
+        self->priv->ucallstat_regex = g_regex_new ("\\r\\n\\+UCALLSTAT:\\s*(\\d+),(\\d+)\\r\\n",
+                                                   G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+
+    ports[0] = mm_base_modem_peek_port_primary   (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
+        if (!ports[i])
+            continue;
+
+        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
+                                                       self->priv->ucallstat_regex,
+                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn)ucallstat_received : NULL,
+                                                       enable ? self : NULL,
+                                                       NULL);
+    }
+}
+
+/*****************************************************************************/
+/* Cleanup unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_cleanup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                               GAsyncResult       *res,
+                                               GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_cleanup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                               GAsyncResult      *res,
+                                               GTask             *task)
+{
+    GError *error = NULL;
+
+    if (!iface_modem_voice_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_cleanup_unsolicited_events (MMIfaceModemVoice   *self,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* our own cleanup first */
+    common_voice_setup_cleanup_unsolicited_events (MM_BROADBAND_MODEM_UBLOX (self), FALSE);
+
+    /* Chain up parent's cleanup */
+    iface_modem_voice_parent->cleanup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_cleanup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
+/* Setup unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_setup_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                             GAsyncResult       *res,
+                                             GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_voice_setup_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                             GAsyncResult      *res,
+                                             GTask             *task)
+{
+    GError  *error = NULL;
+
+    if (!iface_modem_voice_parent->cleanup_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup parent voice unsolicited events: %s", error->message);
+        g_error_free (error);
+    }
+
+    /* our own setup next */
+    common_voice_setup_cleanup_unsolicited_events (MM_BROADBAND_MODEM_UBLOX (self), TRUE);
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_setup_unsolicited_events (MMIfaceModemVoice   *self,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* chain up parent's setup first */
+    iface_modem_voice_parent->setup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_voice_setup_unsolicited_events_ready,
+        task);
+}
+
+/*****************************************************************************/
 /* Create call (Voice interface) */
 
 static MMBaseCall *
@@ -1050,8 +1244,12 @@
              MMCallDirection    direction,
              const gchar       *number)
 {
-    /* New u-blox Call */
-    return mm_call_ublox_new (MM_BASE_MODEM (self), direction, number);
+    return mm_base_call_new (MM_BASE_MODEM (self),
+                             direction,
+                             number,
+                             TRUE,  /* skip_incoming_timeout */
+                             TRUE,  /* supports_dialing_to_ringing */
+                             TRUE); /* supports_ringing_to_active */
 }
 
 /*****************************************************************************/
@@ -1445,6 +1643,10 @@
 {
     iface_modem_voice_parent = g_type_interface_peek_parent (iface);
 
+    iface->setup_unsolicited_events = modem_voice_setup_unsolicited_events;
+    iface->setup_unsolicited_events_finish = modem_voice_setup_unsolicited_events_finish;
+    iface->cleanup_unsolicited_events = modem_voice_cleanup_unsolicited_events;
+    iface->cleanup_unsolicited_events_finish = modem_voice_cleanup_unsolicited_events_finish;
     iface->enable_unsolicited_events = modem_voice_enable_unsolicited_events;
     iface->enable_unsolicited_events_finish = modem_voice_enable_unsolicited_events_finish;
     iface->disable_unsolicited_events = modem_voice_disable_unsolicited_events;
@@ -1460,6 +1662,8 @@
 
     g_regex_unref (self->priv->pbready_regex);
 
+    if (self->priv->ucallstat_regex)
+        g_regex_unref (self->priv->ucallstat_regex);
     g_free (self->priv->operator_id);
 
     G_OBJECT_CLASS (mm_broadband_modem_ublox_parent_class)->finalize (object);
diff --git a/plugins/ublox/mm-call-ublox.c b/plugins/ublox/mm-call-ublox.c
deleted file mode 100644
index f9011fa..0000000
--- a/plugins/ublox/mm-call-ublox.c
+++ /dev/null
@@ -1,194 +0,0 @@
-/* -*- 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) 2018 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-log.h"
-#include "mm-base-modem-at.h"
-#include "mm-broadband-modem-ublox.h"
-#include "mm-call-ublox.h"
-
-G_DEFINE_TYPE (MMCallUblox, mm_call_ublox, MM_TYPE_BASE_CALL)
-
-struct _MMCallUbloxPrivate {
-    GRegex *ucallstat_regex;
-};
-
-/*****************************************************************************/
-/* In-call unsolicited events */
-
-static void
-ublox_ucallstat_received (MMPortSerialAt *port,
-                          GMatchInfo     *match_info,
-                          MMCallUblox    *self)
-{
-    static const gchar *call_stat_names[] = {
-        [0] = "active",
-        [1] = "hold",
-        [2] = "dialling (MO)",
-        [3] = "alerting (MO)",
-        [4] = "ringing (MT)",
-        [5] = "waiting (MT)",
-        [6] = "disconnected",
-        [7] = "connected",
-    };
-    guint id;
-    guint stat;
-
-    if (!mm_get_uint_from_match_info (match_info, 1, &id))
-        return;
-
-    if (!mm_get_uint_from_match_info (match_info, 2, &stat))
-        return;
-
-    if (stat < G_N_ELEMENTS (call_stat_names))
-        mm_dbg ("u-blox call id '%u' state: '%s'", id, call_stat_names[stat]);
-    else
-        mm_dbg ("u-blox call id '%u' state unknown: '%u'", id, stat);
-
-    switch (stat) {
-    case 0:
-        /* ringing to active */
-        if (mm_gdbus_call_get_state (MM_GDBUS_CALL (self)) == MM_CALL_STATE_RINGING_OUT)
-            mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
-        break;
-    case 1:
-        /* ignore hold state, we don't support this yet. */
-        break;
-    case 2:
-        /* ignore dialing state, we already handle this in the parent */
-        break;
-    case 3:
-        /* dialing to ringing */
-        if (mm_gdbus_call_get_state (MM_GDBUS_CALL (self)) == MM_CALL_STATE_DIALING)
-            mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_RINGING_OUT, MM_CALL_STATE_REASON_OUTGOING_STARTED);
-        break;
-    case 4:
-        /* ignore MT ringing state, we already handle this in the parent */
-        break;
-    case 5:
-        /* ignore MT waiting state */
-        break;
-    case 6:
-        mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);
-        break;
-    case 7:
-        /* ringing to active */
-        if (mm_gdbus_call_get_state (MM_GDBUS_CALL (self)) == MM_CALL_STATE_RINGING_OUT)
-            mm_base_call_change_state (MM_BASE_CALL (self), MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
-        break;
-    }
-}
-
-static gboolean
-common_setup_cleanup_unsolicited_events (MMCallUblox  *self,
-                                         gboolean      enable,
-                                         GError      **error)
-{
-    MMBaseModem    *modem = NULL;
-    MMPortSerialAt *port;
-
-    if (G_UNLIKELY (!self->priv->ucallstat_regex))
-        self->priv->ucallstat_regex = g_regex_new ("\\r\\n\\+UCALLSTAT:\\s*(\\d+),(\\d+)\\r\\n",
-						   G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-
-    g_object_get (self,
-                  MM_BASE_CALL_MODEM, &modem,
-                  NULL);
-
-    port = mm_base_modem_peek_port_primary (MM_BASE_MODEM (modem));
-    if (port) {
-        mm_dbg ("%s +UCALLSTAT URC handler", enable ? "adding" : "removing");
-        mm_port_serial_at_add_unsolicited_msg_handler (
-            port,
-            self->priv->ucallstat_regex,
-            enable ? (MMPortSerialAtUnsolicitedMsgFn)ublox_ucallstat_received : NULL,
-            enable ? self : NULL,
-            NULL);
-    }
-
-    g_object_unref (modem);
-    return TRUE;
-}
-
-static gboolean
-setup_unsolicited_events (MMBaseCall  *self,
-                          GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (MM_CALL_UBLOX (self), TRUE, error);
-}
-
-static gboolean
-cleanup_unsolicited_events (MMBaseCall  *self,
-                            GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (MM_CALL_UBLOX (self), FALSE, error);
-}
-
-/*****************************************************************************/
-
-MMBaseCall *
-mm_call_ublox_new (MMBaseModem     *modem,
-                   MMCallDirection  direction,
-                   const gchar     *number)
-{
-    return MM_BASE_CALL (g_object_new (MM_TYPE_CALL_UBLOX,
-                                       MM_BASE_CALL_MODEM, modem,
-                                       "direction",        direction,
-                                       "number",           number,
-                                       MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, TRUE,
-                                       MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE,  TRUE,
-                                       NULL));
-}
-
-static void
-mm_call_ublox_init (MMCallUblox *self)
-{
-    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_CALL_UBLOX, MMCallUbloxPrivate);
-}
-
-static void
-finalize (GObject *object)
-{
-    MMCallUblox *self = MM_CALL_UBLOX (object);
-
-    if (self->priv->ucallstat_regex)
-        g_regex_unref (self->priv->ucallstat_regex);
-
-    G_OBJECT_CLASS (mm_call_ublox_parent_class)->finalize (object);
-}
-
-static void
-mm_call_ublox_class_init (MMCallUbloxClass *klass)
-{
-    GObjectClass    *object_class    = G_OBJECT_CLASS     (klass);
-    MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass);
-
-    g_type_class_add_private (object_class, sizeof (MMCallUbloxPrivate));
-
-    object_class->finalize = finalize;
-
-    base_call_class->setup_unsolicited_events   = setup_unsolicited_events;
-    base_call_class->cleanup_unsolicited_events = cleanup_unsolicited_events;
-}
diff --git a/plugins/ublox/mm-call-ublox.h b/plugins/ublox/mm-call-ublox.h
deleted file mode 100644
index abacb50..0000000
--- a/plugins/ublox/mm-call-ublox.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/* -*- 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) 2018 Aleksander Morgado <aleksander@aleksander.es>
- */
-
-#ifndef MM_CALL_UBLOX_H
-#define MM_CALL_UBLOX_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include "mm-base-call.h"
-
-#define MM_TYPE_CALL_UBLOX            (mm_call_ublox_get_type ())
-#define MM_CALL_UBLOX(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_CALL_UBLOX, MMCallUblox))
-#define MM_CALL_UBLOX_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_CALL_UBLOX, MMCallUbloxClass))
-#define MM_IS_CALL_UBLOX(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_CALL_UBLOX))
-#define MM_IS_CALL_UBLOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_CALL_UBLOX))
-#define MM_CALL_UBLOX_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_CALL_UBLOX, MMCallUbloxClass))
-
-typedef struct _MMCallUblox MMCallUblox;
-typedef struct _MMCallUbloxClass MMCallUbloxClass;
-typedef struct _MMCallUbloxPrivate MMCallUbloxPrivate;
-
-struct _MMCallUblox {
-    MMBaseCall parent;
-    MMCallUbloxPrivate *priv;
-};
-
-struct _MMCallUbloxClass {
-    MMBaseCallClass parent;
-};
-
-GType mm_call_ublox_get_type (void);
-
-MMBaseCall *mm_call_ublox_new (MMBaseModem     *modem,
-                               MMCallDirection  direction,
-                               const gchar     *number);
-
-#endif /* MM_CALL_UBLOX_H */
diff --git a/plugins/ublox/mm-modem-helpers-ublox.c b/plugins/ublox/mm-modem-helpers-ublox.c
index 8608960..df6b6a2 100644
--- a/plugins/ublox/mm-modem-helpers-ublox.c
+++ b/plugins/ublox/mm-modem-helpers-ublox.c
@@ -1578,6 +1578,10 @@
         g_array_unref (nums);
     }
 
+    if (!bands)
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "No known band selection values matched in +UACT response: '%s'", response);
+
     return bands;
 }
 
diff --git a/plugins/ublox/tests/test-modem-helpers-ublox.c b/plugins/ublox/tests/test-modem-helpers-ublox.c
index e0a7d9a..5fad8a7 100644
--- a/plugins/ublox/tests/test-modem-helpers-ublox.c
+++ b/plugins/ublox/tests/test-modem-helpers-ublox.c
@@ -26,6 +26,8 @@
 #include "mm-modem-helpers.h"
 #include "mm-modem-helpers-ublox.h"
 
+#include "test-helpers.h"
+
 /*****************************************************************************/
 /* Test +UPINCNT responses */
 
@@ -488,36 +490,6 @@
 /* Test +UBANDSEL? response parser */
 
 static void
-common_compare_bands (GArray            *bands,
-                      const MMModemBand *expected_bands,
-                      guint              n_expected_bands)
-{
-    gchar  *bands_str;
-    GArray *expected_bands_array;
-    gchar  *expected_bands_str;
-
-    if (!expected_bands || !n_expected_bands) {
-        g_assert (!bands);
-        return;
-    }
-
-    g_assert (bands);
-    mm_common_bands_garray_sort (bands);
-    bands_str = mm_common_build_bands_string ((MMModemBand *)(bands->data), bands->len);
-    g_array_unref (bands);
-
-    expected_bands_array = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), n_expected_bands);
-    g_array_append_vals (expected_bands_array, expected_bands, n_expected_bands);
-    mm_common_bands_garray_sort (expected_bands_array);
-    expected_bands_str = mm_common_build_bands_string ((MMModemBand *)(expected_bands_array->data), expected_bands_array->len);
-    g_array_unref (expected_bands_array);
-
-    g_assert_cmpstr (bands_str, ==, expected_bands_str);
-    g_free (bands_str);
-    g_free (expected_bands_str);
-}
-
-static void
 common_validate_ubandsel_response (const gchar       *str,
                                    const MMModemBand *expected_bands,
                                    const gchar       *model,
@@ -530,7 +502,8 @@
     g_assert_no_error (error);
     g_assert (bands);
 
-    common_compare_bands (bands, expected_bands, n_expected_bands);
+    mm_test_helpers_compare_bands (bands, expected_bands, n_expected_bands);
+    g_array_unref (bands);
 }
 
 static void
@@ -651,14 +624,23 @@
     GArray *bands;
 
     bands = mm_ublox_parse_uact_response (str, &error);
-    g_assert_no_error (error);
 
-    common_compare_bands (bands, expected_bands, n_expected_bands);
+    if (n_expected_bands > 0) {
+        g_assert (bands);
+        g_assert_no_error (error);
+        mm_test_helpers_compare_bands (bands, expected_bands, n_expected_bands);
+        g_array_unref (bands);
+    } else {
+        g_assert (!bands);
+        g_assert (error);
+        g_error_free (error);
+    }
 }
 
 static void
 test_uact_response_empty_list (void)
 {
+    common_validate_uact_response ("", NULL, 0);
     common_validate_uact_response ("+UACT: ,,,\r\n", NULL, 0);
 }
 
@@ -723,9 +705,15 @@
     g_assert_no_error (error);
     g_assert (result);
 
-    common_compare_bands (bands_2g, expected_bands_2g, n_expected_bands_2g);
-    common_compare_bands (bands_3g, expected_bands_3g, n_expected_bands_3g);
-    common_compare_bands (bands_4g, expected_bands_4g, n_expected_bands_4g);
+    mm_test_helpers_compare_bands (bands_2g, expected_bands_2g, n_expected_bands_2g);
+    if (bands_2g)
+        g_array_unref (bands_2g);
+    mm_test_helpers_compare_bands (bands_3g, expected_bands_3g, n_expected_bands_3g);
+    if (bands_3g)
+        g_array_unref (bands_3g);
+    mm_test_helpers_compare_bands (bands_4g, expected_bands_4g, n_expected_bands_4g);
+    if (bands_4g)
+        g_array_unref (bands_4g);
 }
 
 static void
diff --git a/plugins/wavecom/mm-broadband-modem-wavecom.c b/plugins/wavecom/mm-broadband-modem-wavecom.c
index 51e5045..47980d8 100644
--- a/plugins/wavecom/mm-broadband-modem-wavecom.c
+++ b/plugins/wavecom/mm-broadband-modem-wavecom.c
@@ -261,7 +261,7 @@
     r = g_regex_new ("\\r\\n\\+WWSM: ([0-2])(,([0-2]))?.*$", 0, 0, NULL);
     g_assert (r != NULL);
 
-    if (g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, response, 0, &match_info)) {
         guint allowed = 0;
 
         if (mm_get_uint_from_match_info (match_info, 1, &allowed)) {
diff --git a/plugins/xmm/mm-shared-xmm.c b/plugins/xmm/mm-shared-xmm.c
index cbc7c7b..d23a657 100644
--- a/plugins/xmm/mm-shared-xmm.c
+++ b/plugins/xmm/mm-shared-xmm.c
@@ -40,7 +40,8 @@
 typedef enum {
     GPS_ENGINE_STATE_OFF,
     GPS_ENGINE_STATE_STANDALONE,
-    GPS_ENGINE_STATE_ASSISTED,
+    GPS_ENGINE_STATE_AGPS_MSA,
+    GPS_ENGINE_STATE_AGPS_MSB,
 } GpsEngineState;
 
 typedef struct {
@@ -855,7 +856,7 @@
 
         if (transport_protocol_supl_supported && ms_assisted_based_position_mode_supported) {
             mm_dbg ("XLCSLSR based A-GPS control supported");
-            priv->supported_sources |= MM_MODEM_LOCATION_SOURCE_AGPS;
+            priv->supported_sources |= (MM_MODEM_LOCATION_SOURCE_AGPS_MSA | MM_MODEM_LOCATION_SOURCE_AGPS_MSB);
         } else {
             mm_dbg ("XLCSLSR based A-GPS control unsupported: protocol supl %s, ms assisted/based %s",
                     transport_protocol_supl_supported         ? "supported" : "unsupported",
@@ -870,6 +871,18 @@
 }
 
 static void
+run_xlcslsr_test (GTask *task)
+{
+    mm_base_modem_at_command (
+        MM_BASE_MODEM (g_task_get_source_object (task)),
+        "+XLCSLSR=?",
+        3,
+        TRUE, /* allow caching */
+        (GAsyncReadyCallback)xlcslsr_test_ready,
+        task);
+}
+
+static void
 parent_load_capabilities_ready (MMIfaceModemLocation *self,
                                 GAsyncResult         *res,
                                 GTask                *task)
@@ -897,14 +910,7 @@
 
     /* Cache sources supported by the parent */
     g_task_set_task_data (task, GUINT_TO_POINTER (sources), NULL);
-
-    mm_base_modem_at_command (
-        MM_BASE_MODEM (g_task_get_source_object (task)),
-        "+XLCSLSR=?",
-        3,
-        TRUE, /* allow caching */
-        (GAsyncReadyCallback)xlcslsr_test_ready,
-        task);
+    run_xlcslsr_test (task);
 }
 
 void
@@ -919,8 +925,14 @@
     task = g_task_new (self, NULL, callback, user_data);
 
     g_assert (priv->iface_modem_location_parent);
-    g_assert (priv->iface_modem_location_parent->load_capabilities);
-    g_assert (priv->iface_modem_location_parent->load_capabilities_finish);
+
+    if (!priv->iface_modem_location_parent->load_capabilities ||
+        !priv->iface_modem_location_parent->load_capabilities_finish) {
+        /* no parent capabilities */
+        g_task_set_task_data (task, GUINT_TO_POINTER (MM_MODEM_LOCATION_SOURCE_NONE), NULL);
+        run_xlcslsr_test (task);
+        return;
+    }
 
     priv->iface_modem_location_parent->load_capabilities (self,
                                                           (GAsyncReadyCallback)parent_load_capabilities_ready,
@@ -1058,7 +1070,11 @@
             transport_protocol = 2;
             pos_mode = 3;
             break;
-        case GPS_ENGINE_STATE_ASSISTED:
+        case GPS_ENGINE_STATE_AGPS_MSB:
+            transport_protocol = 1;
+            pos_mode = 1;
+            break;
+        case GPS_ENGINE_STATE_AGPS_MSA:
             transport_protocol = 1;
             pos_mode = 2;
             break;
@@ -1069,8 +1085,8 @@
 
     /*
      * AT+XLCSLSR
-     *    transport_protocol:  2 (invalid)     or 1 (supl)
-     *    pos_mode:            3 (standalone)  or 2 (ms assisted+based)
+     *    transport_protocol:  2 (invalid) or 1 (supl)
+     *    pos_mode:            3 (standalone), 1 (msb) or 2 (msa)
      *    client_id:           <empty>
      *    client_id_type:      <empty>
      *    mlc_number:          <empty>
@@ -1191,9 +1207,12 @@
 {
     /* If at lease one of GPS nmea/raw sources enabled, engine started */
     if (sources & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA | MM_MODEM_LOCATION_SOURCE_GPS_RAW)) {
-        /* If A-GPS is enabled, ASSISTED mode */
-        if (sources & MM_MODEM_LOCATION_SOURCE_AGPS)
-            return GPS_ENGINE_STATE_ASSISTED;
+        /* If MSA A-GPS is enabled, MSA mode */
+        if (sources & MM_MODEM_LOCATION_SOURCE_AGPS_MSA)
+            return GPS_ENGINE_STATE_AGPS_MSA;
+        /* If MSB A-GPS is enabled, MSB mode */
+        if (sources & MM_MODEM_LOCATION_SOURCE_AGPS_MSB)
+            return GPS_ENGINE_STATE_AGPS_MSB;
         /* Otherwise, STANDALONE */
         return GPS_ENGINE_STATE_STANDALONE;
     }
@@ -1290,7 +1309,8 @@
     /* We only expect GPS sources here */
     g_assert (source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
                         MM_MODEM_LOCATION_SOURCE_GPS_RAW  |
-                        MM_MODEM_LOCATION_SOURCE_AGPS));
+                        MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                        MM_MODEM_LOCATION_SOURCE_AGPS_MSB));
 
     /* Update engine based on the expected sources */
     gps_engine_state_select (MM_SHARED_XMM (self),
@@ -1366,12 +1386,12 @@
 
     priv = get_private (MM_SHARED_XMM (self));
     g_assert (priv->iface_modem_location_parent);
-    g_assert (priv->iface_modem_location_parent->enable_location_gathering);
-    g_assert (priv->iface_modem_location_parent->enable_location_gathering_finish);
 
     /* Only consider request if it applies to one of the sources we are
      * supporting, otherwise run parent enable */
-    if (!(priv->supported_sources & source)) {
+    if (priv->iface_modem_location_parent->enable_location_gathering &&
+        priv->iface_modem_location_parent->enable_location_gathering_finish &&
+        !(priv->supported_sources & source)) {
         priv->iface_modem_location_parent->enable_location_gathering (self,
                                                                       source,
                                                                       (GAsyncReadyCallback)parent_enable_location_gathering_ready,
@@ -1382,7 +1402,8 @@
     /* We only expect GPS sources here */
     g_assert (source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
                         MM_MODEM_LOCATION_SOURCE_GPS_RAW  |
-                        MM_MODEM_LOCATION_SOURCE_AGPS));
+                        MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                        MM_MODEM_LOCATION_SOURCE_AGPS_MSB));
 
     /* Update engine based on the expected sources */
     gps_engine_state_select (MM_SHARED_XMM (self),
diff --git a/po/LINGUAS b/po/LINGUAS
index 27395f0..68225ab 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -6,9 +6,11 @@
 hu
 id
 it
+lt
 pl
 pt_BR
 sk
 sv
 tr
 uk
+zh_CN
diff --git a/po/lt.po b/po/lt.po
new file mode 100644
index 0000000..ded731e
--- /dev/null
+++ b/po/lt.po
@@ -0,0 +1,110 @@
+# Lithuanian translation for ModemManager.
+# Copyright (C) 2019 ModemManager's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Moo, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: modemmanager-devel@lists.freedesktop.org\n"
+"POT-Creation-Date: 2019-04-25 10:07+0200\n"
+"PO-Revision-Date: 2019-04-13 22:08+0300\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: lt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.2.1\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && (n%100<11 || n%100>19) ? 0 : n"
+"%10>=2 && n%10<=9 && (n%100<11 || n%100>19) ? 1 : 2);\n"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:13
+msgid "Control the Modem Manager daemon"
+msgstr "Valdyti modemo tvarkytuvės tarnybą"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:14
+msgid "System policy prevents controlling the Modem Manager."
+msgstr "Sistemos politika neleidžia valdyti modemo tvarkytuvę."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:22
+msgid "Unlock and control a mobile broadband device"
+msgstr "Atrakinti ir valdyti mobiliojo plačiajuosčio ryšio įrenginį"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:23
+msgid ""
+"System policy prevents unlocking or controlling the mobile broadband device."
+msgstr ""
+"Sistemos politika neleidžia atrakinti ir valdyti mobiliojo plačiajuosčio "
+"ryšio įrenginį."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:31
+msgid "Add, modify, and delete mobile broadband contacts"
+msgstr ""
+"Pridėti, modifikuoti bei ištrinti mobiliojo plačiajuosčio ryšio kontaktus"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:32
+msgid ""
+"System policy prevents adding, modifying, or deleting this device's contacts."
+msgstr ""
+"Sistemos politika neleidžia pridėti, modifikuoti ar ištrinti šio įrenginio "
+"kontaktus."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:40
+msgid "Send, save, modify, and delete text messages"
+msgstr "Siųsti, įrašyti, modifikuoti bei ištrinti tekstines žinutes"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:41
+msgid ""
+"System policy prevents sending or manipulating this device's text messages."
+msgstr ""
+"Sistemos politika neleidžia siųsti ar valdyti šio įrenginio tekstines "
+"žinutes."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:49
+msgid "Accept incoming voice calls or start outgoing voice calls."
+msgstr ""
+"Atsiliepti į gaunamus balso skambučius ar inicijuoti išsiunčiamuosius balso "
+"skambučius."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:50
+msgid "System policy prevents voice calls."
+msgstr "Sistemos politika neleidžia balso skambučių."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:58
+msgid "Enable and view geographic location and positioning information"
+msgstr "Įjungti ir rodyti geografinės vietos bei pozicionavimo informaciją"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:59
+msgid ""
+"System policy prevents enabling or viewing geographic location information."
+msgstr ""
+"Sistemos politika neleidžia įjungti ar rodyti geografinės vietos informaciją."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:67
+msgid "Query and utilize network information and services"
+msgstr "Užklausti ir panaudoti tinklo informacija bei paslaugas"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:68
+msgid ""
+"System policy prevents querying or utilizing network information and "
+"services."
+msgstr ""
+"Sistemos politika neleidžia užklausti ar panaudoti tinklo informacija ir "
+"paslaugas."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:76
+msgid "Query and manage firmware on a mobile broadband device"
+msgstr ""
+"Užklausti ir tvarkyti programinę aparatinę įrangą mobiliojo plačiajuosčio "
+"ryšio įrenginyje"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:77
+msgid "System policy prevents querying or managing this device's firmware."
+msgstr ""
+"Sistemos politika neleidžia užklausti ar tvarkyti šio įrenginio programinę "
+"aparatinę įrangą."
+
+#: src/mm-sleep-monitor.c:114
+msgid "ModemManager needs to reset devices"
+msgstr "ModemManager turi atstatyti įrenginius"
diff --git a/po/zh_CN.po b/po/zh_CN.po
new file mode 100644
index 0000000..4a998d2
--- /dev/null
+++ b/po/zh_CN.po
@@ -0,0 +1,93 @@
+# Chinese (China) translation for ModemManager.
+# Copyright (C) 2019 ModemManager's COPYRIGHT HOLDER
+# This file is distributed under the same license as the ModemManager package.
+# Estel Zhang <callmebedrockdigger@gmail.com>, 2018.
+# 王滋涵 Zephyr Waitzman <i@wi24rd.ml>, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ModemManager master\n"
+"Report-Msgid-Bugs-To: modemmanager-devel@lists.freedesktop.org\n"
+"POT-Creation-Date: 2019-07-02 09:46+0200\n"
+"PO-Revision-Date: 2019-05-03 00:10+0800\n"
+"Last-Translator: 王滋涵 <i@wi24rd.ml>\n"
+"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.2.1\n"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:13
+msgid "Control the Modem Manager daemon"
+msgstr "控制调制解调器管理器守护进程"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:14
+msgid "System policy prevents controlling the Modem Manager."
+msgstr "系统策略禁止控制调制解调器管理器。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:22
+msgid "Unlock and control a mobile broadband device"
+msgstr "解锁并控制移动宽带设备"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:23
+msgid ""
+"System policy prevents unlocking or controlling the mobile broadband device."
+msgstr "系统策略禁止解锁或控制移动宽带设备。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:31
+msgid "Add, modify, and delete mobile broadband contacts"
+msgstr "添加、修改或删除移动宽带联系人"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:32
+msgid ""
+"System policy prevents adding, modifying, or deleting this device's contacts."
+msgstr "系统策略禁止添加、修改或删除移动宽带联系人。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:40
+msgid "Send, save, modify, and delete text messages"
+msgstr "发送、保存修改或删除文本消息"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:41
+msgid ""
+"System policy prevents sending or manipulating this device's text messages."
+msgstr "系统策略禁止发送或操作此设备的文本消息。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:49
+msgid "Accept incoming voice calls or start outgoing voice calls."
+msgstr "接听传入的语音呼叫或者开始传出语音呼叫。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:50
+msgid "System policy prevents voice calls."
+msgstr "系统策略禁止语音呼叫。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:58
+msgid "Enable and view geographic location and positioning information"
+msgstr "启用和查看地理位置和定位信息"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:59
+msgid ""
+"System policy prevents enabling or viewing geographic location information."
+msgstr "系统策略禁止启用和查看地理位置和定位信息。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:67
+msgid "Query and utilize network information and services"
+msgstr "查询和利用网络信息和服务"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:68
+msgid ""
+"System policy prevents querying or utilizing network information and "
+"services."
+msgstr "系统策略禁止查询和利用网络信息和服务。"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:76
+msgid "Query and manage firmware on a mobile broadband device"
+msgstr "在移动宽带设备上查询和管理固件"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:77
+msgid "System policy prevents querying or managing this device's firmware."
+msgstr "系统策略禁止在移动宽带设备上查询和管理固件。"
+
+#: src/mm-sleep-monitor.c:114
+msgid "ModemManager needs to reset devices"
+msgstr "调制解调器管理器需要重置设备"
diff --git a/src/77-mm-pcmcia-device-blacklist.rules b/src/77-mm-pcmcia-device-blacklist.rules
index b496a8e..7c82fa9 100644
--- a/src/77-mm-pcmcia-device-blacklist.rules
+++ b/src/77-mm-pcmcia-device-blacklist.rules
@@ -4,6 +4,6 @@
 SUBSYSTEM!="pcmcia", GOTO="mm_pcmcia_device_blacklist_end"
 
 # Gemplus Serial Port smartcard adapter
-ATTRS{prod_id1}=="Gemplus", ATTRS{prod_id2}=="SerialPort", ATTRS{prod_id3}=="GemPC Card", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{prod_id1}=="Gemplus", ATTRS{prod_id2}=="SerialPort", ATTRS{prod_id3}=="GemPC Card", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 LABEL="mm_pcmcia_device_blacklist_end"
diff --git a/src/77-mm-usb-device-blacklist.rules b/src/77-mm-usb-device-blacklist.rules
index e8d7a4c..983fe43 100644
--- a/src/77-mm-usb-device-blacklist.rules
+++ b/src/77-mm-usb-device-blacklist.rules
@@ -4,208 +4,208 @@
 SUBSYSTEM!="usb", GOTO="mm_usb_device_blacklist_end"
 
 # APC UPS devices
-ATTRS{idVendor}=="051d", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="051d", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Sweex 1000VA
-ATTRS{idVendor}=="0925", ATTRS{idProduct}=="1234", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0925", ATTRS{idProduct}=="1234", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Agiler UPS
-ATTRS{idVendor}=="05b8", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="05b8", ATTRS{idProduct}=="0000", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Krauler UP-M500VA
-ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0000", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Ablerex 625L USB
-ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="0000", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Belkin F6C1200-UNV
-ATTRS{idVendor}=="0665", ATTRS{idProduct}=="5161", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0665", ATTRS{idProduct}=="5161", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Various Liebert and Phoenixtec Power devices
-ATTRS{idVendor}=="06da", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="06da", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Unitek Alpha 1200Sx
-ATTRS{idVendor}=="0f03", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0f03", ATTRS{idProduct}=="0001", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Various Tripplite devices
-ATTRS{idVendor}=="09ae", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="09ae", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Various MGE Office Protection Systems devices
-ATTRS{idVendor}=="0463", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="0463", ATTRS{idProduct}=="ffff", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0463", ATTRS{idProduct}=="0001", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="0463", ATTRS{idProduct}=="ffff", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # CyberPower 900AVR/BC900D
-ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0005", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0005", ENV{ID_MM_TTY_BLACKLIST}="1"
 # CyberPower CP1200AVR/BC1200D
-ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0501", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0501", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Various Belkin devices
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0980", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0900", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0910", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0912", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0551", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0751", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0375", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="1100", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0980", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0900", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0910", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0912", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0551", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0751", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0375", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="1100", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # HP R/T 2200 INTL (like SMART2200RMXL2U)
-ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="1f0a", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="1f0a", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Powerware devices
-ATTRS{idVendor}=="0592", ATTRS{idProduct}=="0002", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0592", ATTRS{idProduct}=="0002", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Palm Treo 700/900/etc
 # Shouldn't be probed themselves, but you can install programs like
 # "MobileStream USB Modem" which changes the USB PID of the device to something
 # that isn't blacklisted.
-ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0061", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0061", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # GlobalScaleTechnologies SheevaPlug
-ATTRS{idVendor}=="9e88", ATTRS{idProduct}=="9e8f", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="9e88", ATTRS{idProduct}=="9e8f", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Atmel Corp at91sam SAMBA bootloader
-ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from the Swiss Federal Institute of Technology
-ATTRS{idVendor}=="0617", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0617", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # West Mountain Radio devices
-ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814a", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814b", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="2405", ATTRS{idProduct}=="0003", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814a", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814b", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="2405", ATTRS{idProduct}=="0003", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Arduinos
-ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9207", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9208", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="2341", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="2a03", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9207", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9208", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Adafruit Flora
-ATTRS{idVendor}=="239a", ATTRS{idProduct}=="0004", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8004", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="239a", ATTRS{idProduct}=="0004", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8004", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from Pololu Corporation
 # except some possible future products.
-ATTRS{idVendor}=="1ffb", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ad", ENV{ID_MM_DEVICE_IGNORE}="0"
-ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ae", ENV{ID_MM_DEVICE_IGNORE}="0"
+ATTRS{idVendor}=="1ffb", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ad", ENV{ID_MM_TTY_BLACKLIST}="0"
+ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ae", ENV{ID_MM_TTY_BLACKLIST}="0"
 
 # Altair U-Boot device
-ATTRS{idVendor}=="0216", ATTRS{idProduct}=="0051", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0216", ATTRS{idProduct}=="0051", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Bluegiga BLE112B
-ATTRS{idVendor}=="2458", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="2458", ATTRS{idProduct}=="0001", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # MediaTek GPS chip (HOLUX M-1200E, GlobalTop Gms-d1, etc)
-ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="3329", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="3329", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # MediaTek MT65xx preloader
-ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="2000", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="2000", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # PS-360 OEM (GPS sold with MS Street and Trips 2005)
-ATTRS{idVendor}=="067b", ATTRS{idProduct}=="aaa0", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="067b", ATTRS{idProduct}=="aaa0", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # u-blox AG, u-blox 5 GPS chips
-ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a5", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a6", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a7", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a5", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a6", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a7", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Garmin GPS devices
-DRIVERS=="garmin_gps", ENV{ID_MM_DEVICE_IGNORE}="1"
+DRIVERS=="garmin_gps", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Garmin ANT+ stick
-ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1009", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1009", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Cypress M8-based GPS devices, UPSes, and serial converters
-DRIVERS=="cypress_m8", ENV{ID_MM_DEVICE_IGNORE}="1"
+DRIVERS=="cypress_m8", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices in the Openmoko vendor ID, except usb hubs
-ATTRS{idVendor}=="1d50", ATTRS{bDeviceClass}!="09", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1d50", ATTRS{bDeviceClass}!="09", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from 3D Robotics
-ATTRS{idVendor}=="26ac", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="26ac", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # empiriKit science lab controller device
-ATTRS{idVendor}=="0425", ATTRS{idProduct}=="0408", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0425", ATTRS{idProduct}=="0408", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Infineon Flashloader used by Intel XMM modem bootloader
-ATTRS{idVendor}=="8087", ATTRS{idProduct}=="0716", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="8087", ATTRS{idProduct}=="0801", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="8087", ATTRS{idProduct}=="0716", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="8087", ATTRS{idProduct}=="0801", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Intel coredump downloader device
-ATTRS{idVendor}=="1519", ATTRS{idProduct}=="f000", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1519", ATTRS{idProduct}=="f000", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # GW Instek AFG-2225 arbitrary function generator
-ATTRS{idVendor}=="2184", ATTRS{idProduct}=="001c", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="2184", ATTRS{idProduct}=="001c", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # PalmOS devices - even though some are phones, they are so old they most
 # likely are not being used anymore
-DRIVERS=="visor", ENV{ID_MM_DEVICE_IGNORE}="1"
+DRIVERS=="visor", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Palmconnect
-ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0080", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0080", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # IMC flashing device
-ATTRS{idVendor}=="058b", ATTRS{idProduct}=="0041", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="058b", ATTRS{idProduct}=="0041", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from the Access Interfacing Solutions (Access Ltd)
 # Access IS do not produce modems and are unlikely to do so in future
-ATTRS{idVendor}=="0db5", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0db5", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Palm M500
-ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0001", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Palm M505
-ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0002", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0002", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Palm M515
-ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0003", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0003", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from POSNET POLSKA S.A.
 # POSNET POLSKA S.A. do not produce modems and are unlikely to do so in future
-ATTRS{idVendor}=="1424", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1424", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # proxmark3
-ATTRS{manufacturer}=="proxmark.org", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{manufacturer}=="proxmark.org", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Sigma Sport Docking Station TOPLINE 2009
-ATTRS{idVendor}=="1d9d", ATTRS{idProduct}=="1010", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1d9d", ATTRS{idProduct}=="1010", ENV{ID_MM_TTY_BLACKLIST}="1"
 # Sigma Sport Docking Station TOPLINE 2012
-ATTRS{idVendor}=="1d9d", ATTRS{idProduct}=="1011", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1d9d", ATTRS{idProduct}=="1011", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Telit LE866 flashing device
-ATTRS{idVendor}=="216f", ATTRS{idProduct}=="0051", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="216f", ATTRS{idProduct}=="0051", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Analog Devices ADALM-PLUTO (PlutoSDR)
-ATTRS{idVendor}=="0456", ATTRS{idProduct}=="b673", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0456", ATTRS{idProduct}=="b673", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Renesas development and promotion boards
-ATTRS{idVendor}=="045B", ATTRS{idProduct}=="0212", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="0409", ATTRS{idProduct}=="0063", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="045B", ATTRS{idProduct}=="0212", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="0409", ATTRS{idProduct}=="0063", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Analog Devices EVAL-ADXL362Z-DB
-ATTRS{idVendor}=="064B", ATTRS{idProduct}=="7825", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="064B", ATTRS{idProduct}=="7825", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # keyboard.io devices
-ATTRS{idVendor}=="1209", ATTRS{idProduct}=="2300", ENV{ID_MM_DEVICE_IGNORE}="1"
-ATTRS{idVendor}=="1209", ATTRS{idProduct}=="2301", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1209", ATTRS{idProduct}=="2300", ENV{ID_MM_TTY_BLACKLIST}="1"
+ATTRS{idVendor}=="1209", ATTRS{idProduct}=="2301", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Netchip Technology, Inc. Linux-USB Serial Gadget (CDC ACM mode)
-ATTRS{idVendor}=="0525", ATTRS{idProduct}=="a4a7", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0525", ATTRS{idProduct}=="a4a7", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Silicon Labs Telegesis ETRX USB Zigbee dongle
-ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="000f", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="000f", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # Devices using Microchip's VID
 # Dangerous Prototypes Bus Pirate v4
-ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="fb00", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="fb00", ENV{ID_MM_TTY_BLACKLIST}="1"
 # Pycom Pysense
-ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="f012", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="f012", ENV{ID_MM_TTY_BLACKLIST}="1"
 # Pycom Pytrack
-ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="f013", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="f013", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 # All devices from Prusa Research
-ATTRS{idVendor}=="2c99", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="2c99", ENV{ID_MM_TTY_BLACKLIST}="1"
 
 LABEL="mm_usb_device_blacklist_end"
diff --git a/src/77-mm-usb-serial-adapters-greylist.rules b/src/77-mm-usb-serial-adapters-greylist.rules
index 2dc6f7d..935275d 100644
--- a/src/77-mm-usb-serial-adapters-greylist.rules
+++ b/src/77-mm-usb-serial-adapters-greylist.rules
@@ -4,47 +4,47 @@
 SUBSYSTEM!="usb", GOTO="mm_usb_serial_adapters_greylist_end"
 
 # Belkin F5U183 Serial Adapter
-ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0103", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0103", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # FTDI-based serial adapters
 #   FTDI does USB to serial converter ICs; and it's very likely that they'll
 #   never do modems themselves, so it should be safe to add a rule only based
 #   on the vendor Id.
-ATTRS{idVendor}=="0403", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="0403", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Devices using Microchip's VID
-ATTRS{idVendor}=="04d8", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04d8", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # ATEN Intl UC-232A (Prolific)
-ATTRS{idVendor}=="0557", ATTRS{idProduct}=="2008", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="0557", ATTRS{idProduct}=="2008", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Prolific USB to Serial adapter
-ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Magic Control Technology Corp adapters
-ATTRS{idVendor}=="0711", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="0711", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Cygnal Integrated Products, Inc. CP210x
-ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea71", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea71", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # QinHeng Electronics HL-340
-ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Atmel Corp. LUFA USB to Serial Adapter Project (Arduino)
-ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="204b", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="204b", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Netchip Technology, Inc. Linux-USB Serial Gadget (CDC ACM mode)
-ATTRS{idVendor}=="0525", ATTRS{idProduct}=="a4a7", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="0525", ATTRS{idProduct}=="a4a7", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 # Cypress Serial-USB devices
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0002", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0003", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0004", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0005", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0006", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0007", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0009", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
-ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="000A", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0002", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0003", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0004", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0005", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0006", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0007", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="0009", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="04B4", ATTRS{idProduct}=="000A", ENV{ID_MM_TTY_MANUAL_SCAN_ONLY}="1"
 
 LABEL="mm_usb_serial_adapters_greylist_end"
diff --git a/src/Makefile.am b/src/Makefile.am
index 7d7bc2e..8f46d2c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -267,6 +267,7 @@
 
 ModemManager_CPPFLAGS = \
 	-DPLUGINDIR=\"$(pkglibdir)\" \
+	-DMM_COMPILATION \
 	$(NULL)
 
 ModemManager_LDADD = \
diff --git a/src/mm-base-bearer.c b/src/mm-base-bearer.c
index 6b7e1e5..9808eb4 100644
--- a/src/mm-base-bearer.c
+++ b/src/mm-base-bearer.c
@@ -82,6 +82,9 @@
     gchar *path;
     /* Status of this bearer */
     MMBearerStatus status;
+    /* Whether we must ignore all disconnection updates if they're
+     * detected by ModemManager itself. */
+    gboolean ignore_disconnection_reports;
     /* Configuration of the bearer */
     MMBearerProperties *config;
     /* Default IP family of this bearer */
@@ -373,6 +376,8 @@
      * interface when going into disconnected state. */
     if (self->priv->status == MM_BEARER_STATUS_DISCONNECTED) {
         bearer_reset_interface_status (self);
+        /* Cleanup flag to ignore disconnection reports */
+        self->priv->ignore_disconnection_reports = FALSE;
         /* Stop statistics */
         bearer_stats_stop (self);
         /* Stop connection monitoring */
@@ -396,6 +401,16 @@
         MM_GDBUS_BEARER (self),
         mm_bearer_ip_config_get_dictionary (ipv6_config));
 
+    /* If PPP is involved in the requested IP config, we must ignore
+     * all disconnection reports found via CGACT? polling or CGEV URCs.
+     * In this case, upper layers should always explicitly disconnect
+     * the bearer when ownership of the TTY is given back to MM. */
+    if ((ipv4_config && mm_bearer_ip_config_get_method (ipv4_config) == MM_BEARER_IP_METHOD_PPP) ||
+        (ipv6_config && mm_bearer_ip_config_get_method (ipv6_config) == MM_BEARER_IP_METHOD_PPP)) {
+        mm_dbg ("PPP is required for connection, will ignore disconnection reports");
+        self->priv->ignore_disconnection_reports = TRUE;
+    }
+
     /* Start statistics */
     bearer_stats_start (self);
 
@@ -709,9 +724,8 @@
         mm_dbg ("Couldn't connect bearer '%s': '%s'",
                 self->priv->path,
                 error->message);
-        if (g_error_matches (error,
-                             MM_CORE_ERROR,
-                             MM_CORE_ERROR_CANCELLED)) {
+        if (   g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED)
+            || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
             /* Will launch disconnection */
             launch_disconnect = TRUE;
         } else
@@ -943,7 +957,7 @@
     GError *error = NULL;
 
     if (!MM_BASE_BEARER_GET_CLASS (self)->disconnect_finish (self, res, &error)) {
-        mm_dbg ("Couldn't disconnect bearer '%s'", self->priv->path);
+        mm_dbg ("Couldn't disconnect bearer '%s': %s", self->priv->path, error->message);
         bearer_update_status (self, MM_BEARER_STATUS_CONNECTED);
         g_task_return_error (task, error);
     }
@@ -1256,13 +1270,46 @@
         bearer_update_status (self, MM_BEARER_STATUS_DISCONNECTED);
 }
 
+/*
+ * This method is used exclusively in two different scenarios:
+ *  a) to report disconnections detected by ModemManager itself (e.g. based on
+ *     CGACT polling or CGEV URCs), applicable to bearers using both NET and
+ *     PPP data ports.
+ *  b) to report failed or successful connection attempts by plugins using NET
+ *     data ports that rely on vendor-specific URCs (e.g. Icera, MBM, Option
+ *     HSO).
+ *
+ * The method is also subclass-able because plugins may require specific
+ * cleanup operations to be done when a bearer is reported as disconnected.
+ * (e.g. the QMI or MBIM implementations require removing signal handlers).
+ *
+ * For all the scenarios involving a) the plugins are required to call the
+ * parent report_connection_status() implementation to report the
+ * DISCONNECTED state. For scenarios involving b) the parent reporting is not
+ * expected at all. In other words, the parent report_connection_status()
+ * is exclusively used in processing disconnections detected by ModemManager
+ * itself.
+ *
+ * If the bearer has been connected and it has required PPP method, we will
+ * ignore all disconnection reports because we cannot disconnect a PPP-based
+ * bearer before the upper layers have stopped using the TTY. In this case,
+ * we must wait for upper layers to detect the disconnection themselves (e.g.
+ * pppd should detect it) and disconnect the bearer through DBus.
+ */
 void
-mm_base_bearer_report_connection_status (MMBaseBearer *self,
-                                         MMBearerConnectionStatus status)
+mm_base_bearer_report_connection_status (MMBaseBearer             *self,
+                                         MMBearerConnectionStatus  status)
 {
+    if ((status == MM_BEARER_CONNECTION_STATUS_DISCONNECTED) && self->priv->ignore_disconnection_reports) {
+        mm_dbg ("ignoring disconnection report for bearer '%s'", self->priv->path);
+        return;
+    }
+
     return MM_BASE_BEARER_GET_CLASS (self)->report_connection_status (self, status);
 }
 
+/*****************************************************************************/
+
 static void
 set_property (GObject *object,
               guint prop_id,
diff --git a/src/mm-base-call.c b/src/mm-base-call.c
index 60795fc..0d09a09 100644
--- a/src/mm-base-call.c
+++ b/src/mm-base-call.c
@@ -11,6 +11,8 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
+ * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #include <config.h>
@@ -40,6 +42,7 @@
     PROP_PATH,
     PROP_CONNECTION,
     PROP_MODEM,
+    PROP_SKIP_INCOMING_TIMEOUT,
     PROP_SUPPORTS_DIALING_TO_RINGING,
     PROP_SUPPORTS_RINGING_TO_ACTIVE,
     PROP_LAST
@@ -55,90 +58,20 @@
     /* The path where the call object is exported */
     gchar *path;
     /* Features */
+    gboolean skip_incoming_timeout;
     gboolean supports_dialing_to_ringing;
     gboolean supports_ringing_to_active;
 
     guint incoming_timeout;
-    GRegex *in_call_events;
 
     /* The port used for audio while call is ongoing, if known */
     MMPort *audio_port;
+
+    /* Ongoing call index */
+    guint index;
 };
 
 /*****************************************************************************/
-/* In-call unsolicited events
- * Once a call is started, we may need to detect special URCs to trigger call
- * state changes, e.g. "NO CARRIER" when the remote party hangs up. */
-
-static void
-in_call_event_received (MMPortSerialAt *port,
-                        GMatchInfo     *info,
-                        MMBaseCall     *self)
-{
-    gchar *str;
-
-    str = g_match_info_fetch (info, 1);
-    if (!str)
-        return;
-
-    if (!strcmp (str, "NO DIALTONE"))
-        mm_base_call_change_state (self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_ERROR);
-    else if (!strcmp (str, "NO CARRIER") || !strcmp (str, "BUSY") || !strcmp (str, "NO ANSWER"))
-        mm_base_call_change_state (self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_REFUSED_OR_BUSY);
-
-    g_free (str);
-}
-
-static gboolean
-common_setup_cleanup_unsolicited_events (MMBaseCall  *self,
-                                         gboolean     enable,
-                                         GError     **error)
-{
-    MMBaseModem    *modem = NULL;
-    MMPortSerialAt *ports[2];
-    gint            i;
-
-    if (G_UNLIKELY (!self->priv->in_call_events))
-        self->priv->in_call_events = g_regex_new ("\\r\\n(NO CARRIER|BUSY|NO ANSWER|NO DIALTONE)\\r\\n$",
-                                                  G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
-
-    g_object_get (self,
-                  MM_BASE_CALL_MODEM, &modem,
-                  NULL);
-
-    ports[0] = mm_base_modem_peek_port_primary   (modem);
-    ports[1] = mm_base_modem_peek_port_secondary (modem);
-
-    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
-        if (!ports[i])
-            continue;
-
-        mm_port_serial_at_add_unsolicited_msg_handler (ports[i],
-                                                       self->priv->in_call_events,
-                                                       enable ? (MMPortSerialAtUnsolicitedMsgFn) in_call_event_received : NULL,
-                                                       enable ? self : NULL,
-                                                       NULL);
-    }
-
-    g_object_unref (modem);
-    return TRUE;
-}
-
-static gboolean
-setup_unsolicited_events (MMBaseCall  *self,
-                          GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (self, TRUE, error);
-}
-
-static gboolean
-cleanup_unsolicited_events (MMBaseCall  *self,
-                            GError     **error)
-{
-    return common_setup_cleanup_unsolicited_events (self, FALSE, error);
-}
-
-/*****************************************************************************/
 /* Incoming calls are reported via RING URCs. If the caller stops the call
  * attempt before it has been answered, the only thing we would see is that the
  * URCs are no longer received. So, we will start a timeout whenever a new RING
@@ -161,6 +94,9 @@
 void
 mm_base_call_incoming_refresh (MMBaseCall *self)
 {
+    if (self->priv->skip_incoming_timeout)
+        return;
+
     if (self->priv->incoming_timeout)
         g_source_remove (self->priv->incoming_timeout);
     self->priv->incoming_timeout = g_timeout_add_seconds (INCOMING_TIMEOUT_SECS, (GSourceFunc)incoming_timeout_cb, self);
@@ -169,10 +105,10 @@
 /*****************************************************************************/
 /* Update audio settings */
 
-static void
-update_audio_settings (MMBaseCall        *self,
-                       MMPort            *audio_port,
-                       MMCallAudioFormat *audio_format)
+void
+mm_base_call_change_audio_settings (MMBaseCall        *self,
+                                    MMPort            *audio_port,
+                                    MMCallAudioFormat *audio_format)
 {
     if (!audio_port && self->priv->audio_port && mm_port_get_connected (self->priv->audio_port))
         mm_port_set_connected (self->priv->audio_port, FALSE);
@@ -206,50 +142,6 @@
 }
 
 static void
-call_started (HandleStartContext *ctx)
-{
-    mm_info ("call is started");
-
-    /* If dialing to ringing supported, leave it dialing */
-    if (!ctx->self->priv->supports_dialing_to_ringing) {
-        /* If ringing to active supported, set it ringing */
-        if (ctx->self->priv->supports_ringing_to_active)
-            mm_base_call_change_state (ctx->self, MM_CALL_STATE_RINGING_OUT, MM_CALL_STATE_REASON_OUTGOING_STARTED);
-        else
-            /* Otherwise, active right away */
-            mm_base_call_change_state (ctx->self, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_OUTGOING_STARTED);
-    }
-    mm_gdbus_call_complete_start (MM_GDBUS_CALL (ctx->self), ctx->invocation);
-    handle_start_context_free (ctx);
-}
-
-static void
-start_setup_audio_channel_ready (MMBaseCall         *self,
-                                 GAsyncResult       *res,
-                                 HandleStartContext *ctx)
-{
-    MMPort            *audio_port = NULL;
-    MMCallAudioFormat *audio_format = NULL;
-    GError            *error = NULL;
-
-    if (!MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel_finish (self, res, &audio_port, &audio_format, &error)) {
-        mm_warn ("Couldn't setup audio channel: '%s'", error->message);
-        mm_base_call_change_state (self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_AUDIO_SETUP_FAILED);
-        g_dbus_method_invocation_take_error (ctx->invocation, error);
-        handle_start_context_free (ctx);
-        return;
-    }
-
-    if (audio_port || audio_format) {
-        update_audio_settings (self, audio_port, audio_format);
-        g_clear_object (&audio_port);
-        g_clear_object (&audio_format);
-    }
-
-    call_started (ctx);
-}
-
-static void
 handle_start_ready (MMBaseCall         *self,
                     GAsyncResult       *res,
                     HandleStartContext *ctx)
@@ -272,17 +164,19 @@
         return;
     }
 
-    /* If there is an audio setup method, run it now */
-    if (MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel) {
-        mm_info ("setting up audio channel...");
-        MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel (self,
-                                                            (GAsyncReadyCallback) start_setup_audio_channel_ready,
-                                                            ctx);
-        return;
-    }
+    mm_info ("call is started");
 
-    /* Otherwise, we're done */
-    call_started (ctx);
+    /* If dialing to ringing supported, leave it dialing */
+    if (!ctx->self->priv->supports_dialing_to_ringing) {
+        /* If ringing to active supported, set it ringing */
+        if (ctx->self->priv->supports_ringing_to_active)
+            mm_base_call_change_state (ctx->self, MM_CALL_STATE_RINGING_OUT, MM_CALL_STATE_REASON_OUTGOING_STARTED);
+        else
+            /* Otherwise, active right away */
+            mm_base_call_change_state (ctx->self, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_OUTGOING_STARTED);
+    }
+    mm_gdbus_call_complete_start (MM_GDBUS_CALL (ctx->self), ctx->invocation);
+    handle_start_context_free (ctx);
 }
 
 static void
@@ -371,46 +265,6 @@
 }
 
 static void
-call_accepted (HandleAcceptContext *ctx)
-{
-    mm_info ("call is accepted");
-
-    if (ctx->self->priv->incoming_timeout) {
-        g_source_remove (ctx->self->priv->incoming_timeout);
-        ctx->self->priv->incoming_timeout = 0;
-    }
-    mm_base_call_change_state (ctx->self, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
-    mm_gdbus_call_complete_accept (MM_GDBUS_CALL (ctx->self), ctx->invocation);
-    handle_accept_context_free (ctx);
-}
-
-static void
-accept_setup_audio_channel_ready (MMBaseCall          *self,
-                                  GAsyncResult        *res,
-                                  HandleAcceptContext *ctx)
-{
-    MMPort            *audio_port = NULL;
-    MMCallAudioFormat *audio_format = NULL;
-    GError            *error = NULL;
-
-    if (!MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel_finish (self, res, &audio_port, &audio_format, &error)) {
-        mm_warn ("Couldn't setup audio channel: '%s'", error->message);
-        mm_base_call_change_state (self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_AUDIO_SETUP_FAILED);
-        g_dbus_method_invocation_take_error (ctx->invocation, error);
-        handle_accept_context_free (ctx);
-        return;
-    }
-
-    if (audio_port || audio_format) {
-        update_audio_settings (self, audio_port, audio_format);
-        g_clear_object (&audio_port);
-        g_clear_object (&audio_format);
-    }
-
-    call_accepted (ctx);
-}
-
-static void
 handle_accept_ready (MMBaseCall *self,
                      GAsyncResult *res,
                      HandleAcceptContext *ctx)
@@ -424,17 +278,15 @@
         return;
     }
 
-    /* If there is an audio setup method, run it now */
-    if (MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel) {
-        mm_info ("setting up audio channel...");
-        MM_BASE_CALL_GET_CLASS (self)->setup_audio_channel (self,
-                                                            (GAsyncReadyCallback) accept_setup_audio_channel_ready,
-                                                            ctx);
-        return;
-    }
+    mm_info ("call is accepted");
 
-    /* Otherwise, we're done */
-    call_accepted (ctx);
+    if (ctx->self->priv->incoming_timeout) {
+        g_source_remove (ctx->self->priv->incoming_timeout);
+        ctx->self->priv->incoming_timeout = 0;
+    }
+    mm_base_call_change_state (ctx->self, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
+    mm_gdbus_call_complete_accept (MM_GDBUS_CALL (ctx->self), ctx->invocation);
+    handle_accept_context_free (ctx);
 }
 
 static void
@@ -503,7 +355,265 @@
 }
 
 /*****************************************************************************/
+/* Deflect call (DBus call handling) */
 
+typedef struct {
+    MMBaseCall            *self;
+    MMBaseModem           *modem;
+    GDBusMethodInvocation *invocation;
+    gchar                 *number;
+} HandleDeflectContext;
+
+static void
+handle_deflect_context_free (HandleDeflectContext *ctx)
+{
+    g_free (ctx->number);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->modem);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleDeflectContext, ctx);
+}
+
+static void
+handle_deflect_ready (MMBaseCall           *self,
+                      GAsyncResult         *res,
+                      HandleDeflectContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!MM_BASE_CALL_GET_CLASS (self)->deflect_finish (self, res, &error)) {
+        mm_base_call_change_state (self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_ERROR);
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_deflect_context_free (ctx);
+        return;
+    }
+
+    mm_info ("call is deflected to '%s'", ctx->number);
+    mm_base_call_change_state (ctx->self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_DEFLECTED);
+    mm_gdbus_call_complete_deflect (MM_GDBUS_CALL (ctx->self), ctx->invocation);
+    handle_deflect_context_free (ctx);
+}
+
+static void
+handle_deflect_auth_ready (MMBaseModem          *modem,
+                           GAsyncResult         *res,
+                           HandleDeflectContext *ctx)
+{
+    MMCallState state;
+    GError *error = NULL;
+
+    if (!mm_base_modem_authorize_finish (modem, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_deflect_context_free (ctx);
+        return;
+    }
+
+    state = mm_gdbus_call_get_state (MM_GDBUS_CALL (ctx->self));
+
+    /* We can only deflect incoming call in ringing or waiting state */
+    if (state != MM_CALL_STATE_RINGING_IN && state != MM_CALL_STATE_WAITING) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_FAILED,
+                                               "This call was not ringing/waiting, cannot deflect");
+        handle_deflect_context_free (ctx);
+        return;
+    }
+
+    mm_info ("user request to deflect call");
+
+    /* Check if we do support doing it */
+    if (!MM_BASE_CALL_GET_CLASS (ctx->self)->deflect ||
+        !MM_BASE_CALL_GET_CLASS (ctx->self)->deflect_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Deflecting call is not supported by this modem");
+        handle_deflect_context_free (ctx);
+        return;
+    }
+
+    MM_BASE_CALL_GET_CLASS (ctx->self)->deflect (ctx->self,
+                                                 ctx->number,
+                                                 (GAsyncReadyCallback)handle_deflect_ready,
+                                                 ctx);
+}
+
+static gboolean
+handle_deflect (MMBaseCall            *self,
+                GDBusMethodInvocation *invocation,
+                const gchar           *number)
+{
+    HandleDeflectContext *ctx;
+
+    ctx = g_slice_new0 (HandleDeflectContext);
+    ctx->self       = g_object_ref (self);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->number     = g_strdup (number);
+    g_object_get (self,
+                  MM_BASE_CALL_MODEM, &ctx->modem,
+                  NULL);
+
+    mm_base_modem_authorize (ctx->modem,
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_deflect_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Join multiparty call (DBus call handling) */
+
+typedef struct {
+    MMBaseCall            *self;
+    MMBaseModem           *modem;
+    GDBusMethodInvocation *invocation;
+} HandleJoinMultipartyContext;
+
+static void
+handle_join_multiparty_context_free (HandleJoinMultipartyContext *ctx)
+{
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->modem);
+    g_object_unref (ctx->self);
+    g_free (ctx);
+}
+
+static void
+modem_voice_join_multiparty_ready (MMIfaceModemVoice           *modem,
+                                   GAsyncResult                *res,
+                                   HandleJoinMultipartyContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!mm_iface_modem_voice_join_multiparty_finish (modem, res, &error))
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+    else
+        mm_gdbus_call_complete_join_multiparty (MM_GDBUS_CALL (ctx->self), ctx->invocation);
+    handle_join_multiparty_context_free (ctx);
+}
+
+static void
+handle_join_multiparty_auth_ready (MMBaseModem                  *modem,
+                                   GAsyncResult                 *res,
+                                   HandleJoinMultipartyContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!mm_base_modem_authorize_finish (modem, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_join_multiparty_context_free (ctx);
+        return;
+    }
+
+    /* This action is provided in the Call API, but implemented in the Modem.Voice interface
+     * logic, because the action affects not only one call object, but all call objects that
+     * are part of the multiparty call. */
+    mm_iface_modem_voice_join_multiparty (MM_IFACE_MODEM_VOICE (ctx->modem),
+                                          ctx->self,
+                                          (GAsyncReadyCallback)modem_voice_join_multiparty_ready,
+                                          ctx);
+}
+
+static gboolean
+handle_join_multiparty (MMBaseCall            *self,
+                        GDBusMethodInvocation *invocation)
+{
+    HandleJoinMultipartyContext *ctx;
+
+    ctx = g_new0 (HandleJoinMultipartyContext, 1);
+    ctx->self = g_object_ref (self);
+    ctx->invocation = g_object_ref (invocation);
+    g_object_get (self,
+                  MM_BASE_CALL_MODEM, &ctx->modem,
+                  NULL);
+
+    mm_base_modem_authorize (ctx->modem,
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_join_multiparty_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Leave multiparty call (DBus call handling) */
+
+typedef struct {
+    MMBaseCall            *self;
+    MMBaseModem           *modem;
+    GDBusMethodInvocation *invocation;
+} HandleLeaveMultipartyContext;
+
+static void
+handle_leave_multiparty_context_free (HandleLeaveMultipartyContext *ctx)
+{
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->modem);
+    g_object_unref (ctx->self);
+    g_free (ctx);
+}
+
+static void
+modem_voice_leave_multiparty_ready (MMIfaceModemVoice            *modem,
+                                    GAsyncResult                 *res,
+                                    HandleLeaveMultipartyContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!mm_iface_modem_voice_leave_multiparty_finish (modem, res, &error))
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+    else
+        mm_gdbus_call_complete_leave_multiparty (MM_GDBUS_CALL (ctx->self), ctx->invocation);
+
+    handle_leave_multiparty_context_free (ctx);
+}
+
+static void
+handle_leave_multiparty_auth_ready (MMBaseModem                  *modem,
+                                    GAsyncResult                 *res,
+                                    HandleLeaveMultipartyContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!mm_base_modem_authorize_finish (modem, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_leave_multiparty_context_free (ctx);
+        return;
+    }
+
+    /* This action is provided in the Call API, but implemented in the Modem.Voice interface
+     * logic, because the action affects not only one call object, but all call objects that
+     * are part of the multiparty call. */
+    mm_iface_modem_voice_leave_multiparty (MM_IFACE_MODEM_VOICE (ctx->modem),
+                                           ctx->self,
+                                           (GAsyncReadyCallback)modem_voice_leave_multiparty_ready,
+                                           ctx);
+}
+
+static gboolean
+handle_leave_multiparty (MMBaseCall            *self,
+                         GDBusMethodInvocation *invocation)
+{
+    HandleLeaveMultipartyContext *ctx;
+
+    ctx = g_new0 (HandleLeaveMultipartyContext, 1);
+    ctx->self = g_object_ref (self);
+    ctx->invocation = g_object_ref (invocation);
+    g_object_get (self,
+                  MM_BASE_CALL_MODEM, &ctx->modem,
+                  NULL);
+
+    mm_base_modem_authorize (ctx->modem,
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_leave_multiparty_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
 /* Hangup call (DBus call handling) */
 
 typedef struct {
@@ -534,10 +644,7 @@
     if (!MM_BASE_CALL_GET_CLASS (self)->hangup_finish (self, res, &error))
         g_dbus_method_invocation_take_error (ctx->invocation, error);
     else {
-        if (ctx->self->priv->incoming_timeout) {
-            g_source_remove (ctx->self->priv->incoming_timeout);
-            ctx->self->priv->incoming_timeout = 0;
-        }
+        /* note: timeouts are already removed when setting state as TERMINATED */
         mm_gdbus_call_complete_hangup (MM_GDBUS_CALL (ctx->self), ctx->invocation);
     }
 
@@ -742,21 +849,14 @@
     GError *error = NULL;
 
     /* Handle method invocations */
-    g_signal_connect (self,
-                      "handle-start",
-                      G_CALLBACK (handle_start),
-                      NULL);
-    g_signal_connect (self,
-                      "handle-accept",
-                      G_CALLBACK (handle_accept),
-                      NULL);
-    g_signal_connect (self,
-                      "handle-hangup",
-                      G_CALLBACK (handle_hangup),
-                      NULL);
-    g_signal_connect (self,
-                      "handle-send-dtmf",
-                      G_CALLBACK (handle_send_dtmf),
+    g_object_connect (self,
+                      "signal::handle-start",            G_CALLBACK (handle_start),            NULL,
+                      "signal::handle-accept",           G_CALLBACK (handle_accept),           NULL,
+                      "signal::handle-deflect",          G_CALLBACK (handle_deflect),          NULL,
+                      "signal::handle-join-multiparty",  G_CALLBACK (handle_join_multiparty),  NULL,
+                      "signal::handle-leave-multiparty", G_CALLBACK (handle_leave_multiparty), NULL,
+                      "signal::handle-hangup",           G_CALLBACK (handle_hangup),           NULL,
+                      "signal::handle-send-dtmf",        G_CALLBACK (handle_send_dtmf),        NULL,
                       NULL);
 
     if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self),
@@ -786,32 +886,70 @@
     return self->priv->path;
 }
 
-/* Define the states in which we want to handle in-call events */
-#define MM_CALL_STATE_IS_IN_CALL(state)         \
-    (state == MM_CALL_STATE_DIALING ||          \
-     state == MM_CALL_STATE_RINGING_IN  ||      \
-     state == MM_CALL_STATE_RINGING_OUT ||      \
-     state == MM_CALL_STATE_ACTIVE)
-
-static void
-cleanup_audio_channel_ready (MMBaseCall   *self,
-                             GAsyncResult *res)
+const gchar *
+mm_base_call_get_number (MMBaseCall *self)
 {
-    GError *error = NULL;
-
-    if (!MM_BASE_CALL_GET_CLASS (self)->cleanup_audio_channel_finish (self, res, &error)) {
-        mm_warn ("audio channel cleanup failed: %s", error->message);
-        g_error_free (error);
-    }
+    return mm_gdbus_call_get_number (MM_GDBUS_CALL (self));
 }
 
 void
+mm_base_call_set_number (MMBaseCall  *self,
+                         const gchar *number)
+{
+    return mm_gdbus_call_set_number (MM_GDBUS_CALL (self), number);
+}
+
+MMCallDirection
+mm_base_call_get_direction (MMBaseCall *self)
+{
+    return (MMCallDirection) mm_gdbus_call_get_direction (MM_GDBUS_CALL (self));
+}
+
+MMCallState
+mm_base_call_get_state (MMBaseCall *self)
+{
+    return (MMCallState) mm_gdbus_call_get_state (MM_GDBUS_CALL (self));
+}
+
+gboolean
+mm_base_call_get_multiparty (MMBaseCall *self)
+{
+    return mm_gdbus_call_get_multiparty (MM_GDBUS_CALL (self));
+}
+
+void
+mm_base_call_set_multiparty (MMBaseCall *self,
+                             gboolean    multiparty)
+{
+    return mm_gdbus_call_set_multiparty (MM_GDBUS_CALL (self), multiparty);
+}
+
+/*****************************************************************************/
+/* Current call index, only applicable while the call is ongoing
+ * See 3GPP TS 22.030 [27], subclause 6.5.5.1.
+ */
+
+guint
+mm_base_call_get_index (MMBaseCall *self)
+{
+    return self->priv->index;
+}
+
+void
+mm_base_call_set_index (MMBaseCall *self,
+                        guint       index)
+{
+    self->priv->index = index;
+}
+
+/*****************************************************************************/
+
+void
 mm_base_call_change_state (MMBaseCall        *self,
                            MMCallState        new_state,
                            MMCallStateReason  reason)
 {
-    MMCallState  old_state;
-    GError      *error = NULL;
+    MMCallState old_state;
 
     old_state = mm_gdbus_call_get_state (MM_GDBUS_CALL (self));
 
@@ -824,26 +962,13 @@
              mm_call_state_reason_get_string (reason));
 
     /* Setup/cleanup unsolicited events  based on state transitions to/from ACTIVE */
-    if (!MM_CALL_STATE_IS_IN_CALL (old_state) && MM_CALL_STATE_IS_IN_CALL (new_state)) {
-        mm_dbg ("Setting up in-call unsolicited events...");
-        if (MM_BASE_CALL_GET_CLASS (self)->setup_unsolicited_events &&
-            !MM_BASE_CALL_GET_CLASS (self)->setup_unsolicited_events (self, &error)) {
-            mm_warn ("Couldn't setup in-call unsolicited events: %s", error->message);
-            g_error_free (error);
-        }
-    } else if (MM_CALL_STATE_IS_IN_CALL (old_state) && !MM_CALL_STATE_IS_IN_CALL (new_state)) {
-        mm_dbg ("Cleaning up in-call unsolicited events...");
-        if (MM_BASE_CALL_GET_CLASS (self)->cleanup_unsolicited_events &&
-            !MM_BASE_CALL_GET_CLASS (self)->cleanup_unsolicited_events (self, &error)) {
-            mm_warn ("Couldn't cleanup in-call unsolicited events: %s", error->message);
-            g_error_free (error);
-        }
-        if (MM_BASE_CALL_GET_CLASS (self)->cleanup_audio_channel) {
-            mm_info ("cleaning up audio channel...");
-            update_audio_settings (self, NULL, NULL);
-            MM_BASE_CALL_GET_CLASS (self)->cleanup_audio_channel (self,
-                                                                  (GAsyncReadyCallback) cleanup_audio_channel_ready,
-                                                                  NULL);
+    if (new_state == MM_CALL_STATE_TERMINATED) {
+        /* reset index */
+        self->priv->index = 0;
+        /* cleanup incoming timeout, if any */
+        if (self->priv->incoming_timeout) {
+            g_source_remove (self->priv->incoming_timeout);
+            self->priv->incoming_timeout = 0;
         }
     }
 
@@ -852,6 +977,8 @@
     mm_gdbus_call_emit_state_changed (MM_GDBUS_CALL (self), old_state, new_state, reason);
 }
 
+/*****************************************************************************/
+
 void
 mm_base_call_received_dtmf (MMBaseCall *self,
                             const gchar *dtmf)
@@ -962,6 +1089,53 @@
 }
 
 /*****************************************************************************/
+/* Deflect the call */
+
+static gboolean
+call_deflect_finish (MMBaseCall    *self,
+                     GAsyncResult  *res,
+                     GError       **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+call_deflect_ready (MMBaseModem  *modem,
+                    GAsyncResult *res,
+                    GTask        *task)
+{
+    GError *error = NULL;
+
+    mm_base_modem_at_command_finish (modem, res, &error);
+    if (error)
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+call_deflect (MMBaseCall          *self,
+              const gchar         *number,
+              GAsyncReadyCallback  callback,
+              gpointer             user_data)
+{
+    GTask *task;
+    gchar *cmd;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    cmd = g_strdup_printf ("+CTFR=%s", number);
+    mm_base_modem_at_command (self->priv->modem,
+                              cmd,
+                              20,
+                              FALSE,
+                              (GAsyncReadyCallback)call_deflect_ready,
+                              task);
+    g_free (cmd);
+}
+
+/*****************************************************************************/
 /* Hangup the call */
 
 static gboolean
@@ -973,14 +1147,13 @@
 }
 
 static void
-call_hangup_ready (MMBaseModem  *modem,
-                   GAsyncResult *res,
-                   GTask        *task)
+chup_ready (MMBaseModem  *modem,
+            GAsyncResult *res,
+            GTask        *task)
 {
     GError *error = NULL;
 
     mm_base_modem_at_command_finish (modem, res, &error);
-
     if (error)
         g_task_return_error (task, error);
     else
@@ -989,6 +1162,43 @@
 }
 
 static void
+chup_fallback (GTask *task)
+{
+    MMBaseCall *self;
+
+    self = g_task_get_source_object (task);
+    mm_base_modem_at_command (self->priv->modem,
+                              "+CHUP",
+                              2,
+                              FALSE,
+                              (GAsyncReadyCallback)chup_ready,
+                              task);
+}
+
+static void
+chld_hangup_ready (MMBaseModem  *modem,
+                   GAsyncResult *res,
+                   GTask        *task)
+{
+    MMBaseCall *self;
+    GError     *error = NULL;
+
+    self = g_task_get_source_object (task);
+
+    mm_base_modem_at_command_finish (modem, res, &error);
+    if (error) {
+        mm_warn ("couldn't hangup single call with call id '%u': %s",
+                 self->priv->index, error->message);
+        g_error_free (error);
+        chup_fallback (task);
+        return;
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
 call_hangup (MMBaseCall          *self,
              GAsyncReadyCallback  callback,
              gpointer             user_data)
@@ -996,12 +1206,24 @@
     GTask *task;
 
     task = g_task_new (self, NULL, callback, user_data);
-    mm_base_modem_at_command (self->priv->modem,
-                              "+CHUP",
-                              2,
-                              FALSE,
-                              (GAsyncReadyCallback)call_hangup_ready,
-                              task);
+
+    /* Try to hangup the single call id */
+    if (self->priv->index) {
+        gchar *cmd;
+
+        cmd = g_strdup_printf ("+CHLD=1%u", self->priv->index);
+        mm_base_modem_at_command (self->priv->modem,
+                                  cmd,
+                                  2,
+                                  FALSE,
+                                  (GAsyncReadyCallback)chld_hangup_ready,
+                                  task);
+        g_free (cmd);
+        return;
+    }
+
+    /* otherwise terminate all */
+    chup_fallback (task);
 }
 
 /*****************************************************************************/
@@ -1061,12 +1283,18 @@
 MMBaseCall *
 mm_base_call_new (MMBaseModem     *modem,
                   MMCallDirection  direction,
-                  const gchar     *number)
+                  const gchar     *number,
+                  gboolean         skip_incoming_timeout,
+                  gboolean         supports_dialing_to_ringing,
+                  gboolean         supports_ringing_to_active)
 {
     return MM_BASE_CALL (g_object_new (MM_TYPE_BASE_CALL,
                                        MM_BASE_CALL_MODEM, modem,
                                        "direction",        direction,
                                        "number",           number,
+                                       MM_BASE_CALL_SKIP_INCOMING_TIMEOUT,       skip_incoming_timeout,
+                                       MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, supports_dialing_to_ringing,
+                                       MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE,  supports_ringing_to_active,
                                        NULL));
 }
 
@@ -1112,6 +1340,9 @@
                                     G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
         }
         break;
+    case PROP_SKIP_INCOMING_TIMEOUT:
+        self->priv->skip_incoming_timeout = g_value_get_boolean (value);
+        break;
     case PROP_SUPPORTS_DIALING_TO_RINGING:
         self->priv->supports_dialing_to_ringing = g_value_get_boolean (value);
         break;
@@ -1142,6 +1373,9 @@
     case PROP_MODEM:
         g_value_set_object (value, self->priv->modem);
         break;
+    case PROP_SKIP_INCOMING_TIMEOUT:
+        g_value_set_boolean (value, self->priv->skip_incoming_timeout);
+        break;
     case PROP_SUPPORTS_DIALING_TO_RINGING:
         g_value_set_boolean (value, self->priv->supports_dialing_to_ringing);
         break;
@@ -1166,8 +1400,6 @@
 {
     MMBaseCall *self = MM_BASE_CALL (object);
 
-    if (self->priv->in_call_events)
-        g_regex_unref (self->priv->in_call_events);
     g_free (self->priv->path);
 
     G_OBJECT_CLASS (mm_base_call_parent_class)->finalize (object);
@@ -1214,13 +1446,12 @@
     klass->start_finish               = call_start_finish;
     klass->accept                     = call_accept;
     klass->accept_finish              = call_accept_finish;
+    klass->deflect                    = call_deflect;
+    klass->deflect_finish             = call_deflect_finish;
     klass->hangup                     = call_hangup;
     klass->hangup_finish              = call_hangup_finish;
     klass->send_dtmf                  = call_send_dtmf;
     klass->send_dtmf_finish           = call_send_dtmf_finish;
-    klass->setup_unsolicited_events   = setup_unsolicited_events;
-    klass->cleanup_unsolicited_events = cleanup_unsolicited_events;
-
 
     properties[PROP_CONNECTION] =
         g_param_spec_object (MM_BASE_CALL_CONNECTION,
@@ -1246,6 +1477,14 @@
                              G_PARAM_READWRITE);
     g_object_class_install_property (object_class, PROP_MODEM, properties[PROP_MODEM]);
 
+    properties[PROP_SKIP_INCOMING_TIMEOUT] =
+        g_param_spec_boolean (MM_BASE_CALL_SKIP_INCOMING_TIMEOUT,
+                              "Skip incoming timeout",
+                              "There is no need to setup a timeout for incoming calls",
+                              FALSE,
+                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+    g_object_class_install_property (object_class, PROP_SKIP_INCOMING_TIMEOUT, properties[PROP_SKIP_INCOMING_TIMEOUT]);
+
     properties[PROP_SUPPORTS_DIALING_TO_RINGING] =
         g_param_spec_boolean (MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING,
                               "Dialing to ringing",
diff --git a/src/mm-base-call.h b/src/mm-base-call.h
index edb4aae..973bcaf 100644
--- a/src/mm-base-call.h
+++ b/src/mm-base-call.h
@@ -11,6 +11,7 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #ifndef MM_BASE_CALL_H
@@ -39,6 +40,7 @@
 #define MM_BASE_CALL_PATH                        "call-path"
 #define MM_BASE_CALL_CONNECTION                  "call-connection"
 #define MM_BASE_CALL_MODEM                       "call-modem"
+#define MM_BASE_CALL_SKIP_INCOMING_TIMEOUT       "call-skip-incoming-timeout"
 #define MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING "call-supports-dialing-to-ringing"
 #define MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE  "call-supports-ringing-to-active"
 
@@ -66,6 +68,15 @@
                                 GAsyncResult *res,
                                 GError **error);
 
+    /* Deflect the call */
+    void     (* deflect)        (MMBaseCall           *self,
+                                 const gchar          *number,
+                                 GAsyncReadyCallback   callback,
+                                 gpointer              user_data);
+    gboolean (* deflect_finish) (MMBaseCall           *self,
+                                 GAsyncResult         *res,
+                                 GError              **error);
+
     /* Hangup the call */
     void     (* hangup)        (MMBaseCall *self,
                                 GAsyncReadyCallback callback,
@@ -82,28 +93,6 @@
     gboolean (* send_dtmf_finish) (MMBaseCall *self,
                                    GAsyncResult *res,
                                    GError **error);
-
-    /* Setup/cleanup in-call unsolicited events */
-    gboolean (* setup_unsolicited_events)   (MMBaseCall  *self,
-                                             GError     **error);
-    gboolean (* cleanup_unsolicited_events) (MMBaseCall  *self,
-                                             GError     **error);
-
-    /* Setup/cleanup audio channel */
-    void     (* setup_audio_channel)          (MMBaseCall           *self,
-                                               GAsyncReadyCallback   callback,
-                                               gpointer              user_data);
-    gboolean (* setup_audio_channel_finish)   (MMBaseCall           *self,
-                                               GAsyncResult         *res,
-                                               MMPort              **audio_port,
-                                               MMCallAudioFormat   **audio_format,
-                                               GError              **error);
-    void     (* cleanup_audio_channel)        (MMBaseCall           *self,
-                                               GAsyncReadyCallback   callback,
-                                               gpointer              user_data);
-    gboolean (* cleanup_audio_channel_finish) (MMBaseCall           *self,
-                                               GAsyncResult         *res,
-                                               GError              **error);
 };
 
 GType mm_base_call_get_type (void);
@@ -111,16 +100,36 @@
 /* This one can be overriden by plugins */
 MMBaseCall *mm_base_call_new (MMBaseModem     *modem,
                               MMCallDirection  direction,
-                              const gchar     *number);
+                              const gchar     *number,
+                              gboolean         skip_incoming_timeout,
+                              gboolean         supports_dialing_to_ringing,
+                              gboolean         supports_ringing_to_active);
 
-void         mm_base_call_export   (MMBaseCall *self);
-void         mm_base_call_unexport (MMBaseCall *self);
-const gchar *mm_base_call_get_path (MMBaseCall *self);
+void             mm_base_call_export         (MMBaseCall *self);
+void             mm_base_call_unexport       (MMBaseCall *self);
+
+const gchar     *mm_base_call_get_path       (MMBaseCall *self);
+const gchar     *mm_base_call_get_number     (MMBaseCall *self);
+MMCallDirection  mm_base_call_get_direction  (MMBaseCall *self);
+MMCallState      mm_base_call_get_state      (MMBaseCall *self);
+guint            mm_base_call_get_index      (MMBaseCall *self);
+gboolean         mm_base_call_get_multiparty (MMBaseCall *self);
+
+void             mm_base_call_set_number     (MMBaseCall  *self,
+                                              const gchar *number);
+void             mm_base_call_set_index      (MMBaseCall  *self,
+                                              guint        index);
+void             mm_base_call_set_multiparty (MMBaseCall  *self,
+                                              gboolean     multiparty);
 
 void         mm_base_call_change_state (MMBaseCall *self,
                                         MMCallState new_state,
                                         MMCallStateReason reason);
 
+void         mm_base_call_change_audio_settings (MMBaseCall        *self,
+                                                 MMPort            *audio_port,
+                                                 MMCallAudioFormat *audio_format);
+
 void         mm_base_call_received_dtmf (MMBaseCall *self,
                                          const gchar *dtmf);
 
diff --git a/src/mm-base-modem-at.c b/src/mm-base-modem-at.c
index 05b3be4..b4e0660 100644
--- a/src/mm-base-modem-at.c
+++ b/src/mm-base-modem-at.c
@@ -301,7 +301,7 @@
         ctx->current->command,
         ctx->current->timeout,
         FALSE,
-        FALSE,
+        ctx->current->allow_cached,
         ctx->cancellable,
         (GAsyncReadyCallback)at_sequence_parse_response,
         ctx);
diff --git a/src/mm-base-sim.c b/src/mm-base-sim.c
index 72ad2bb..1cb2459 100644
--- a/src/mm-base-sim.c
+++ b/src/mm-base-sim.c
@@ -122,7 +122,7 @@
     command = g_strdup_printf ("+CPWD=\"SC\",\"%s\",\"%s\"",
                                old_pin,
                                new_pin);
-    mm_base_modem_at_command (MM_BASE_MODEM (self->priv->modem),
+    mm_base_modem_at_command (self->priv->modem,
                               command,
                               3,
                               FALSE,
@@ -291,7 +291,7 @@
     command = g_strdup_printf ("+CLCK=\"SC\",%d,\"%s\"",
                                enabled ? 1 : 0,
                                pin);
-    mm_base_modem_at_command (MM_BASE_MODEM (self->priv->modem),
+    mm_base_modem_at_command (self->priv->modem,
                               command,
                               3,
                               FALSE,
@@ -461,7 +461,7 @@
                g_strdup_printf ("+CPIN=\"%s\",\"%s\"", puk, pin) :
                g_strdup_printf ("+CPIN=\"%s\"", pin));
 
-    mm_base_modem_at_command (MM_BASE_MODEM (self->priv->modem),
+    mm_base_modem_at_command (self->priv->modem,
                               command,
                               3,
                               FALSE,
@@ -1041,7 +1041,7 @@
 
     /* READ BINARY of EFiccid (ICC Identification) ETSI TS 102.221 section 13.2 */
     mm_base_modem_at_command (
-        MM_BASE_MODEM (self->priv->modem),
+        self->priv->modem,
         "+CRSM=176,12258,0,0,10",
         20,
         FALSE,
@@ -1108,7 +1108,7 @@
     mm_dbg ("loading IMSI...");
 
     mm_base_modem_at_command (
-        MM_BASE_MODEM (self->priv->modem),
+        self->priv->modem,
         "+CIMI",
         3,
         FALSE,
@@ -1227,7 +1227,7 @@
 
     /* READ BINARY of EFad (Administrative Data) ETSI 51.011 section 10.3.18 */
     mm_base_modem_at_command (
-        MM_BASE_MODEM (self->priv->modem),
+        self->priv->modem,
         "+CRSM=176,28589,0,0,4",
         10,
         FALSE,
@@ -1322,7 +1322,7 @@
 
     /* READ BINARY of EFspn (Service Provider Name) ETSI 51.011 section 10.3.11 */
     mm_base_modem_at_command (
-        MM_BASE_MODEM (self->priv->modem),
+        self->priv->modem,
         "+CRSM=176,28486,0,0,17",
         10,
         FALSE,
diff --git a/src/mm-base-sms.c b/src/mm-base-sms.c
index 25a0261..6aeabf1 100644
--- a/src/mm-base-sms.c
+++ b/src/mm-base-sms.c
@@ -1,3 +1,4 @@
+
 /* -*- 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
@@ -914,7 +915,11 @@
 
     ctx = g_task_get_task_data (task);
 
-    /* Send the actual message data */
+    /* Send the actual message data.
+     * We send the data as 'raw' data because we do NOT want it to
+     * be treated as an AT command (i.e. we don't want it prefixed
+     * with AT+ and suffixed with <CR><LF>), plus, we want it to be
+     * sent right away (not queued after other AT commands). */
     mm_base_modem_at_command_raw (ctx->modem,
                                   ctx->msg_data,
                                   10,
@@ -1131,10 +1136,14 @@
 
     ctx = g_task_get_task_data (task);
 
-    /* Send the actual message data */
+    /* Send the actual message data.
+     * We send the data as 'raw' data because we do NOT want it to
+     * be treated as an AT command (i.e. we don't want it prefixed
+     * with AT+ and suffixed with <CR><LF>), plus, we want it to be
+     * sent right away (not queued after other AT commands). */
     mm_base_modem_at_command_raw (ctx->modem,
                                   ctx->msg_data,
-                                  10,
+                                  60,
                                   FALSE,
                                   (GAsyncReadyCallback)send_generic_msg_data_ready,
                                   task);
@@ -1205,7 +1214,7 @@
                                mm_sms_part_get_index ((MMSmsPart *)ctx->current->data));
         mm_base_modem_at_command (ctx->modem,
                                   cmd,
-                                  30,
+                                  60,
                                   FALSE,
                                   (GAsyncReadyCallback)send_from_storage_ready,
                                   task);
@@ -1232,7 +1241,7 @@
     g_assert (ctx->msg_data != NULL);
     mm_base_modem_at_command (ctx->modem,
                               cmd,
-                              30,
+                              60,
                               FALSE,
                               (GAsyncReadyCallback)send_generic_ready,
                               task);
@@ -1498,16 +1507,45 @@
 
 /*****************************************************************************/
 
-static gboolean
-assemble_sms (MMBaseSms *self,
-              GError **error)
+static void
+initialize_sms (MMBaseSms *self)
 {
-    GList *l;
-    guint idx;
+    MMSmsPart *part;
+    guint      validity_relative;
+
+    /* Some of the fields of the SMS object may be initialized as soon as we have
+     * one part already available, even if it's not exactly the first one */
+    g_assert (self->priv->parts);
+    part = (MMSmsPart *)(self->priv->parts->data);
+
+    /* Prepare for validity tuple */
+    validity_relative = mm_sms_part_get_validity_relative (part);
+
+    g_object_set (self,
+                  "pdu-type",            mm_sms_part_get_pdu_type (part),
+                  "smsc",                mm_sms_part_get_smsc (part),
+                  "class",               mm_sms_part_get_class (part),
+                  "teleservice-id",      mm_sms_part_get_cdma_teleservice_id (part),
+                  "service-category",    mm_sms_part_get_cdma_service_category (part),
+                  "number",              mm_sms_part_get_number (part),
+                  "validity",            (validity_relative ?
+                                          g_variant_new ("(uv)", MM_SMS_VALIDITY_TYPE_RELATIVE, g_variant_new_uint32 (validity_relative)) :
+                                          g_variant_new ("(uv)", MM_SMS_VALIDITY_TYPE_UNKNOWN, g_variant_new_boolean (FALSE))),
+                  "timestamp",           mm_sms_part_get_timestamp (part),
+                  "discharge-timestamp", mm_sms_part_get_discharge_timestamp (part),
+                  "delivery-state",      mm_sms_part_get_delivery_state (part),
+                  NULL);
+}
+
+static gboolean
+assemble_sms (MMBaseSms  *self,
+              GError    **error)
+{
+    GList      *l;
+    guint       idx;
     MMSmsPart **sorted_parts;
-    GString *fulltext;
+    GString    *fulltext;
     GByteArray *fulldata;
-    guint validity_relative;
 
     sorted_parts = g_new0 (MMSmsPart *, self->priv->max_parts);
 
@@ -1595,30 +1633,15 @@
     /* If we got all parts, we also have the first one always */
     g_assert (sorted_parts[0] != NULL);
 
-    /* Prepare for validity tuple */
-    validity_relative = mm_sms_part_get_validity_relative (sorted_parts[0]);
-
     /* If we got everything, assemble the text! */
     g_object_set (self,
-                  "pdu-type",  mm_sms_part_get_pdu_type (sorted_parts[0]),
-                  "text",      fulltext->str,
-                  "data",      g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
-                                                        fulldata->data,
-                                                        fulldata->len * sizeof (guint8),
-                                                        TRUE,
-                                                        (GDestroyNotify) g_byte_array_unref,
-                                                        g_byte_array_ref (fulldata)),
-                  "smsc",      mm_sms_part_get_smsc (sorted_parts[0]),
-                  "class",     mm_sms_part_get_class (sorted_parts[0]),
-                  "teleservice-id",   mm_sms_part_get_cdma_teleservice_id (sorted_parts[0]),
-                  "service-category", mm_sms_part_get_cdma_service_category (sorted_parts[0]),
-                  "number",    mm_sms_part_get_number (sorted_parts[0]),
-                  "validity",                (validity_relative ?
-                                              g_variant_new ("(uv)", MM_SMS_VALIDITY_TYPE_RELATIVE, g_variant_new_uint32 (validity_relative)) :
-                                              g_variant_new ("(uv)", MM_SMS_VALIDITY_TYPE_UNKNOWN, g_variant_new_boolean (FALSE))),
-                  "timestamp",               mm_sms_part_get_timestamp (sorted_parts[0]),
-                  "discharge-timestamp",     mm_sms_part_get_discharge_timestamp (sorted_parts[0]),
-                  "delivery-state",          mm_sms_part_get_delivery_state (sorted_parts[0]),
+                  "text", fulltext->str,
+                  "data", g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
+                                                   fulldata->data,
+                                                   fulldata->len * sizeof (guint8),
+                                                   TRUE,
+                                                   (GDestroyNotify) g_byte_array_unref,
+                                                   g_byte_array_ref (fulldata)),
                   /* delivery report request and message reference taken always from the last part */
                   "message-reference",       mm_sms_part_get_message_reference (sorted_parts[self->priv->max_parts - 1]),
                   "delivery-report-request", mm_sms_part_get_delivery_report_request (sorted_parts[self->priv->max_parts - 1]),
@@ -1690,6 +1713,10 @@
                                               part,
                                               (GCompareFunc)cmp_sms_part_sequence);
 
+    /* If this is the first part we take, initialize common SMS fields */
+    if (g_list_length (self->priv->parts) == 1)
+        initialize_sms (self);
+
     /* We only populate contents when the multipart SMS is complete */
     if (mm_base_sms_multipart_is_complete (self)) {
         GError *inner_error = NULL;
@@ -1740,6 +1767,9 @@
     /* Keep the single part in the list */
     self->priv->parts = g_list_prepend (self->priv->parts, part);
 
+    /* Initialize common SMS fields */
+    initialize_sms (self);
+
     if (!assemble_sms (self, error)) {
         /* Note: we need to remove the part from the list, as we really didn't
          * take it, and therefore the caller is responsible for freeing it. */
diff --git a/src/mm-bearer-qmi.c b/src/mm-bearer-qmi.c
index 1e37666..8815122 100644
--- a/src/mm-bearer-qmi.c
+++ b/src/mm-bearer-qmi.c
@@ -1698,7 +1698,6 @@
 } DisconnectStep;
 
 typedef struct {
-    MMPort *data;
     DisconnectStep step;
 
     gboolean running_ipv4;
@@ -1723,7 +1722,6 @@
         g_object_unref (ctx->client_ipv4);
     if (ctx->client_ipv6)
         g_object_unref (ctx->client_ipv6);
-    g_object_unref (ctx->data);
     g_slice_free (DisconnectContext, ctx);
 }
 
@@ -1851,9 +1849,10 @@
                                                                            ctx->client_ipv4,
                                                                            FALSE,
                                                                            &self->priv->packet_service_status_ipv4_indication_id);
-            cleanup_event_report_unsolicited_events (self,
-                                                     ctx->client_ipv4,
-                                                     &self->priv->event_report_ipv4_indication_id);
+            if (self->priv->event_report_ipv4_indication_id)
+                cleanup_event_report_unsolicited_events (self,
+                                                         ctx->client_ipv4,
+                                                         &self->priv->event_report_ipv4_indication_id);
 
             input = qmi_message_wds_stop_network_input_new ();
             qmi_message_wds_stop_network_input_set_packet_data_handle (input, ctx->packet_data_handle_ipv4, NULL);
@@ -1880,9 +1879,10 @@
                                                                            ctx->client_ipv6,
                                                                            FALSE,
                                                                            &self->priv->packet_service_status_ipv6_indication_id);
-            cleanup_event_report_unsolicited_events (self,
-                                                     ctx->client_ipv6,
-                                                     &self->priv->event_report_ipv6_indication_id);
+            if (self->priv->event_report_ipv6_indication_id)
+                cleanup_event_report_unsolicited_events (self,
+                                                         ctx->client_ipv6,
+                                                         &self->priv->event_report_ipv6_indication_id);
 
             input = qmi_message_wds_stop_network_input_new ();
             qmi_message_wds_stop_network_input_set_packet_data_handle (input, ctx->packet_data_handle_ipv6, NULL);
@@ -1948,7 +1948,6 @@
     }
 
     ctx = g_slice_new0 (DisconnectContext);
-    ctx->data = g_object_ref (self->priv->data);
     ctx->client_ipv4 = self->priv->client_ipv4 ? g_object_ref (self->priv->client_ipv4) : NULL;
     ctx->packet_data_handle_ipv4 = self->priv->packet_data_handle_ipv4;
     ctx->client_ipv6 = self->priv->client_ipv6 ? g_object_ref (self->priv->client_ipv6) : NULL;
diff --git a/src/mm-broadband-bearer.c b/src/mm-broadband-bearer.c
index aa6654e..cce1fb4 100644
--- a/src/mm-broadband-bearer.c
+++ b/src/mm-broadband-bearer.c
@@ -600,21 +600,34 @@
 /*****************************************************************************/
 /* 3GPP cid selection (sub-step of the 3GPP Connection sequence) */
 
+typedef enum {
+    CID_SELECTION_3GPP_STEP_FIRST,
+    CID_SELECTION_3GPP_STEP_FORMAT,
+    CID_SELECTION_3GPP_STEP_CURRENT,
+    CID_SELECTION_3GPP_STEP_SELECT_CONTEXT,
+    CID_SELECTION_3GPP_STEP_INITIALIZE_CONTEXT,
+    CID_SELECTION_3GPP_STEP_LAST,
+} CidSelection3gppStep;
+
 typedef struct {
-    MMBroadbandBearer *self;
-    MMBaseModem       *modem;
-    MMPortSerialAt    *primary;
-    GCancellable      *cancellable;
-    guint              cid;
-    guint              max_cid;
-    gboolean           use_existing_cid;
-    MMBearerIpFamily   ip_family;
+    CidSelection3gppStep  step;
+    MMBaseModem          *modem;
+    MMPortSerialAt       *primary;
+    GCancellable         *cancellable;
+    GList                *context_list;
+    GList                *context_format_list;
+    guint                 cid;
+    gboolean              cid_reused;
+    gboolean              cid_overwritten;
+    MMBearerIpFamily      ip_family;
+    const gchar          *pdp_type;
 } CidSelection3gppContext;
 
 static void
 cid_selection_3gpp_context_free (CidSelection3gppContext *ctx)
 {
-    g_object_unref (ctx->self);
+    mm_3gpp_pdp_context_format_list_free (ctx->context_format_list);
+    mm_3gpp_pdp_context_list_free (ctx->context_list);
     g_object_unref (ctx->modem);
     g_object_unref (ctx->primary);
     g_object_unref (ctx->cancellable);
@@ -633,211 +646,129 @@
     return (guint) (cid < 0 ? 0 : cid);
 }
 
+static void cid_selection_3gpp_context_step (GTask *task);
+
 static void
-initialize_pdp_context_ready (MMBaseModem  *modem,
-                              GAsyncResult *res,
-                              GTask        *task)
+cgdcont_set_ready (MMBaseModem  *modem,
+                   GAsyncResult *res,
+                   GTask        *task)
 {
     CidSelection3gppContext *ctx;
     GError                  *error = NULL;
 
-    ctx = (CidSelection3gppContext *) g_task_get_task_data (task);
+    ctx = g_task_get_task_data (task);
+
     mm_base_modem_at_command_full_finish (modem, res, &error);
     if (error) {
-        mm_warn ("Couldn't initialize PDP context with our APN: '%s'", error->message);
+        mm_warn ("Couldn't initialize context: '%s'", error->message);
         g_task_return_error (task, error);
-    } else
-        g_task_return_int (task, (gssize) ctx->cid);
-    g_object_unref (task);
+        g_object_unref (task);
+        return;
+    }
+
+    ctx->step++;
+    cid_selection_3gpp_context_step (task);
 }
 
 static void
-find_cid_ready (MMBaseModem  *modem,
-                GAsyncResult *res,
-                GTask        *task)
+cid_selection_3gpp_initialize_context (GTask *task)
 {
-    gchar                   *apn;
-    gchar                   *command;
-    GError                  *error = NULL;
-    const gchar             *pdp_type;
+    MMBroadbandBearer       *self;
     CidSelection3gppContext *ctx;
+    const gchar             *apn;
+    gchar                   *quoted_apn;
+    gchar                   *cmd;
 
-    ctx = (CidSelection3gppContext *) g_task_get_task_data (task);
-
-    mm_base_modem_at_sequence_full_finish (modem, res, NULL, &error);
-    if (error) {
-        mm_warn ("Couldn't find best CID to use: '%s'", error->message);
-        g_task_return_error (task, error);
-        g_object_unref (task);
-        return;
-    }
-
-    /* Validate requested PDP type */
-    pdp_type = mm_3gpp_get_pdp_type_from_ip_family (ctx->ip_family);
-    if (!pdp_type) {
-        gchar * str;
-
-        str = mm_bearer_ip_family_build_string_from_mask (ctx->ip_family);
-        g_task_return_new_error (task,
-                                 MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                                 "Unsupported IP type requested: '%s'", str);
-        g_object_unref (task);
-        g_free (str);
-        return;
-    }
-
-    /* If no error reported, we must have a valid CID to be used */
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
     g_assert (ctx->cid != 0);
+    g_assert (!ctx->cid_reused);
 
-    /* If there's already a PDP context defined, just use it */
-    if (ctx->use_existing_cid) {
-        g_task_return_int (task, (gssize) ctx->cid);
-        g_object_unref (task);
-        return;
-    }
+    /* Initialize a PDP context with our APN and PDP type */
+    apn = mm_bearer_properties_get_apn (mm_base_bearer_peek_config (MM_BASE_BEARER (self)));
+    mm_dbg ("%s context with APN '%s' and PDP type '%s'",
+            ctx->cid_overwritten ? "Overwriting" : "Initializing",
+            apn, ctx->pdp_type);
+    quoted_apn = mm_port_serial_at_quote_string (apn);
+    cmd = g_strdup_printf ("+CGDCONT=%u,\"%s\",%s", ctx->cid, ctx->pdp_type, quoted_apn);
+    g_free (quoted_apn);
 
-    /* Otherwise, initialize a new PDP context with our APN */
-    apn = mm_port_serial_at_quote_string (mm_bearer_properties_get_apn (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self))));
-    command = g_strdup_printf ("+CGDCONT=%u,\"%s\",%s", ctx->cid, pdp_type, apn);
-    g_free (apn);
     mm_base_modem_at_command_full (ctx->modem,
                                    ctx->primary,
-                                   command,
+                                   cmd,
                                    3,
                                    FALSE,
                                    FALSE, /* raw */
                                    NULL, /* cancellable */
-                                   (GAsyncReadyCallback) initialize_pdp_context_ready,
+                                   (GAsyncReadyCallback) cgdcont_set_ready,
                                    task);
-    g_free (command);
+    g_free (cmd);
 }
 
-static gboolean
-parse_cid_range (MMBaseModem              *modem,
-                 CidSelection3gppContext  *ctx,
-                 const gchar              *command,
-                 const gchar              *response,
-                 gboolean                  last_command,
-                 const GError             *error,
-                 GVariant                **result,
-                 GError                  **result_error)
+static void
+cid_selection_3gpp_select_context (GTask *task)
 {
-    GError *inner_error = NULL;
-    GList  *formats, *l;
-    guint   cid;
+    MMBroadbandBearer       *self;
+    CidSelection3gppContext *ctx;
 
-    /* If cancelled, set result error */
-    if (g_cancellable_is_cancelled (ctx->cancellable)) {
-        g_set_error (result_error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED,
-                     "Connection setup operation has been cancelled");
-        return FALSE;
-    }
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
 
-    if (error) {
-        mm_dbg ("Unexpected +CGDCONT error: '%s'", error->message);
-        mm_dbg ("Defaulting to CID=1");
-        ctx->cid = 1;
-        return TRUE;
-    }
+    ctx->cid = mm_3gpp_select_best_cid (mm_bearer_properties_get_apn (mm_base_bearer_peek_config (MM_BASE_BEARER (self))),
+                                        ctx->ip_family,
+                                        ctx->context_list,
+                                        ctx->context_format_list,
+                                        &ctx->cid_reused,
+                                        &ctx->cid_overwritten);
 
-    formats = mm_3gpp_parse_cgdcont_test_response (response, &inner_error);
-    if (inner_error) {
-        mm_dbg ("Error parsing +CGDCONT test response: '%s'", inner_error->message);
-        mm_dbg ("Defaulting to CID=1");
-        g_error_free (inner_error);
-        ctx->cid = 1;
-        return TRUE;
-    }
+    /* At this point, CID must ALWAYS be set */
+    g_assert (ctx->cid);
 
-    cid = 0;
-    for (l = formats; l; l = g_list_next (l)) {
-        MM3gppPdpContextFormat *format = l->data;
-
-        /* Found exact PDP type? */
-        if (format->pdp_type == ctx->ip_family) {
-            gchar *ip_family_str;
-
-            ip_family_str = mm_bearer_ip_family_build_string_from_mask (format->pdp_type);
-            if (ctx->max_cid < format->max_cid) {
-                cid = ctx->max_cid + 1;
-                mm_dbg ("Using empty CID %u with PDP type '%s'", cid, ip_family_str);
-            } else {
-                cid = ctx->max_cid;
-                mm_dbg ("Re-using CID %u (max) with PDP type '%s'", cid, ip_family_str);
-            }
-            g_free (ip_family_str);
-            break;
-        }
-    }
-
-    mm_3gpp_pdp_context_format_list_free (formats);
-
-    if (cid == 0) {
-        mm_dbg ("Defaulting to CID=1");
-        cid = 1;
-    }
-
-    ctx->cid = cid;
-    return TRUE;
+    /* If we're reusing an existing one, no need to initialize it, so we're done */
+    if (ctx->cid_reused) {
+        g_assert (!ctx->cid_overwritten);
+        ctx->step = CID_SELECTION_3GPP_STEP_LAST;
+    } else
+        ctx->step++;
+    cid_selection_3gpp_context_step (task);
 }
 
-static gboolean
-parse_pdp_list (MMBaseModem             *modem,
-                CidSelection3gppContext *ctx,
-                const gchar             *command,
-                const gchar             *response,
-                gboolean                 last_command,
-                const GError            *error,
-                GVariant               **result,
-                GError                 **result_error)
+static void
+cgdcont_query_ready (MMBaseModem  *modem,
+                     GAsyncResult *res,
+                     GTask        *task)
 {
-    GError *inner_error = NULL;
-    GList *pdp_list;
-    GList *l;
-    guint cid;
+    CidSelection3gppContext *ctx;
+    GError                  *error = NULL;
+    const gchar             *response;
+    GList                   *l;
 
-    /* If cancelled, set result error */
-    if (g_cancellable_is_cancelled (ctx->cancellable)) {
-        g_set_error (result_error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED,
-                     "Connection setup operation has been cancelled");
-        return FALSE;
+    ctx = g_task_get_task_data (task);
+
+    response = mm_base_modem_at_command_full_finish (modem, res, &error);
+    if (!response) {
+        /* Ignore errors */
+        mm_dbg ("Failed checking currently defined contexts: %s", error->message);
+        g_clear_error (&error);
+        goto out;
     }
 
-    /* Some Android phones don't support querying existing PDP contexts,
-     * but will accept setting the APN.  So if CGDCONT? isn't supported,
-     * just ignore that error and hope for the best. (bgo #637327)
-     */
-    if (g_error_matches (error,
-                         MM_MOBILE_EQUIPMENT_ERROR,
-                         MM_MOBILE_EQUIPMENT_ERROR_NOT_SUPPORTED)) {
-        mm_dbg ("Querying PDP context list is unsupported");
-        return FALSE;
+    /* Build context list */
+    ctx->context_list = mm_3gpp_parse_cgdcont_read_response (response, &error);
+    if (!ctx->context_list) {
+        if (error) {
+            mm_dbg ("Failed parsing currently defined contexts: %s", error->message);
+            g_clear_error (&error);
+        } else
+            mm_dbg ("No contexts currently defined");
+        goto out;
     }
 
-    if (error) {
-        mm_dbg ("Unexpected +CGDCONT? error: '%s'", error->message);
-        return FALSE;
-    }
-
-    pdp_list = mm_3gpp_parse_cgdcont_read_response (response, &inner_error);
-    if (!pdp_list) {
-        if (inner_error) {
-            mm_dbg ("%s", inner_error->message);
-            g_error_free (inner_error);
-        } else {
-            /* No predefined PDP contexts found */
-            mm_dbg ("No PDP contexts found");
-        }
-        return FALSE;
-    }
-
-    cid = 0;
-
     /* Show all found PDP contexts in debug log */
-    mm_dbg ("Found '%u' PDP contexts", g_list_length (pdp_list));
-    for (l = pdp_list; l; l = g_list_next (l)) {
+    mm_dbg ("Found '%u' PDP contexts", g_list_length (ctx->context_list));
+    for (l = ctx->context_list; l; l = g_list_next (l)) {
         MM3gppPdpContext *pdp = l->data;
-        gchar *ip_family_str;
+        gchar            *ip_family_str;
 
         ip_family_str = mm_bearer_ip_family_build_string_from_mask (pdp->pdp_type);
         mm_dbg ("  PDP context [cid=%u] [type='%s'] [apn='%s']",
@@ -847,56 +778,121 @@
         g_free (ip_family_str);
     }
 
-    /* Look for the exact PDP context we want */
-    for (l = pdp_list; l; l = g_list_next (l)) {
-        MM3gppPdpContext *pdp = l->data;
-
-        if (pdp->pdp_type == ctx->ip_family) {
-            const gchar *apn;
-
-            apn = mm_bearer_properties_get_apn (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));
-
-            /* First requested, then existing */
-            if (mm_3gpp_cmp_apn_name (apn, pdp->apn)) {
-                gchar *ip_family_str;
-
-                /* Found a PDP context with the same APN and PDP type, we'll use it. */
-                ip_family_str = mm_bearer_ip_family_build_string_from_mask (pdp->pdp_type);
-                mm_dbg ("Found PDP context with CID %u and PDP type %s for APN '%s'",
-                        pdp->cid, ip_family_str, apn ? apn : "");
-                cid = pdp->cid;
-                ctx->use_existing_cid = TRUE;
-                g_free (ip_family_str);
-                /* In this case, stop searching */
-                break;
-            }
-
-            /* PDP with no APN set? we may use that one if not exact match found */
-            if (!pdp->apn || !pdp->apn[0]) {
-                mm_dbg ("Found PDP context with CID %u and no APN",
-                        pdp->cid);
-                cid = pdp->cid;
-            }
-        }
-
-        if (ctx->max_cid < pdp->cid)
-            ctx->max_cid = pdp->cid;
-    }
-    mm_3gpp_pdp_context_list_free (pdp_list);
-
-    if (cid > 0) {
-        ctx->cid = cid;
-        return TRUE;
-    }
-
-    return FALSE;
+out:
+    ctx->step++;
+    cid_selection_3gpp_context_step (task);
 }
 
-static const MMBaseModemAtCommand find_cid_sequence[] = {
-    { "+CGDCONT?",  3, FALSE, (MMBaseModemAtResponseProcessor) parse_pdp_list  },
-    { "+CGDCONT=?", 3, TRUE,  (MMBaseModemAtResponseProcessor) parse_cid_range },
-    { NULL }
-};
+static void
+cid_selection_3gpp_query_current (GTask *task)
+{
+    CidSelection3gppContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    mm_dbg ("Checking currently defined contexts...");
+    mm_base_modem_at_command_full (ctx->modem,
+                                   ctx->primary,
+                                   "+CGDCONT?",
+                                   3,
+                                   FALSE, /* cached */
+                                   FALSE, /* raw */
+                                   ctx->cancellable,
+                                   (GAsyncReadyCallback)cgdcont_query_ready,
+                                   task);
+}
+
+static void
+cgdcont_test_ready (MMBaseModem  *modem,
+                    GAsyncResult *res,
+                    GTask        *task)
+{
+    CidSelection3gppContext *ctx;
+    GError                  *error = NULL;
+    const gchar             *response;
+
+    ctx = g_task_get_task_data (task);
+
+    response = mm_base_modem_at_command_full_finish (modem, res, &error);
+    if (!response) {
+        /* Ignore errors */
+        mm_dbg ("Failed checking context definition format: %s", error->message);
+        g_clear_error (&error);
+        goto out;
+    }
+
+    ctx->context_format_list = mm_3gpp_parse_cgdcont_test_response (response, &error);
+    if (error) {
+        mm_dbg ("Error parsing +CGDCONT test response: '%s'", error->message);
+        g_clear_error (&error);
+        goto out;
+    }
+
+out:
+    ctx->step++;
+    cid_selection_3gpp_context_step (task);
+}
+
+static void
+cid_selection_3gpp_query_format (GTask *task)
+{
+    CidSelection3gppContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    mm_dbg ("Checking context definition format...");
+    mm_base_modem_at_command_full (ctx->modem,
+                                   ctx->primary,
+                                   "+CGDCONT=?",
+                                   3,
+                                   TRUE, /* cached */
+                                   FALSE, /* raw */
+                                   ctx->cancellable,
+                                   (GAsyncReadyCallback)cgdcont_test_ready,
+                                   task);
+}
+
+static void
+cid_selection_3gpp_context_step (GTask *task)
+{
+    CidSelection3gppContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    /* Abort if we've been cancelled */
+    if (g_task_return_error_if_cancelled (task)) {
+        g_object_unref (task);
+        return;
+    }
+
+    switch (ctx->step) {
+    case CID_SELECTION_3GPP_STEP_FIRST:
+        /* Fall down to next step */
+        ctx->step++;
+
+    case CID_SELECTION_3GPP_STEP_FORMAT:
+        cid_selection_3gpp_query_format (task);
+        return;
+
+    case CID_SELECTION_3GPP_STEP_CURRENT:
+        cid_selection_3gpp_query_current (task);
+        return;
+
+    case CID_SELECTION_3GPP_STEP_SELECT_CONTEXT:
+        cid_selection_3gpp_select_context (task);
+        return;
+
+    case CID_SELECTION_3GPP_STEP_INITIALIZE_CONTEXT:
+        cid_selection_3gpp_initialize_context (task);
+        return;
+
+    case CID_SELECTION_3GPP_STEP_LAST:
+        g_assert (ctx->cid != 0);
+        g_task_return_int (task, (gssize) ctx->cid);
+        g_object_unref (task);
+        return;
+    }
+}
 
 static void
 cid_selection_3gpp (MMBroadbandBearer   *self,
@@ -909,25 +905,30 @@
     GTask                   *task;
     CidSelection3gppContext *ctx;
 
+    task = g_task_new (self, cancellable, callback, user_data);
+
     ctx = g_slice_new0 (CidSelection3gppContext);
-    ctx->self        = g_object_ref (self);
+    ctx->step        = CID_SELECTION_3GPP_STEP_FIRST;
     ctx->modem       = g_object_ref (modem);
     ctx->primary     = g_object_ref (primary);
     ctx->cancellable = g_object_ref (cancellable);
     ctx->ip_family   = select_bearer_ip_family (self);
-
-    task = g_task_new (self, cancellable, callback, user_data);
     g_task_set_task_data (task, ctx, (GDestroyNotify) cid_selection_3gpp_context_free);
 
-    mm_dbg ("Looking for best CID...");
-    mm_base_modem_at_sequence_full (ctx->modem,
-                                    ctx->primary,
-                                    find_cid_sequence,
-                                    ctx, /* also passed as response processor context */
-                                    NULL, /* response_processor_context_free */
-                                    NULL, /* cancellable */
-                                    (GAsyncReadyCallback) find_cid_ready,
-                                    task);
+    /* Validate PDP type */
+    ctx->pdp_type = mm_3gpp_get_pdp_type_from_ip_family (ctx->ip_family);
+    if (!ctx->pdp_type) {
+        gchar * str;
+
+        str = mm_bearer_ip_family_build_string_from_mask (ctx->ip_family);
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                                 "Unsupported IP type requested: '%s'", str);
+        g_object_unref (task);
+        g_free (str);
+        return;
+    }
+
+    cid_selection_3gpp_context_step (task);
 }
 
 /*****************************************************************************/
@@ -1692,7 +1693,7 @@
         mm_base_modem_at_command_full (ctx->modem,
                                        ctx->primary,
                                        ctx->cgact_command,
-                                       10,
+                                       45,
                                        FALSE,
                                        FALSE, /* raw */
                                        NULL, /* cancellable */
@@ -1711,7 +1712,7 @@
         mm_base_modem_at_command_full (ctx->modem,
                                        ctx->secondary,
                                        ctx->cgact_command,
-                                       10,
+                                       45,
                                        FALSE,
                                        FALSE, /* raw */
                                        NULL, /* cancellable */
diff --git a/src/mm-broadband-modem-mbim.c b/src/mm-broadband-modem-mbim.c
index 1cc4ec2..10d831a 100644
--- a/src/mm-broadband-modem-mbim.c
+++ b/src/mm-broadband-modem-mbim.c
@@ -884,8 +884,8 @@
 /* Unlock required loading (Modem interface) */
 
 typedef struct {
-    guint n_ready_status_checks;
     MbimDevice *device;
+    gboolean    last_attempt;
 } LoadUnlockRequiredContext;
 
 static void
@@ -1008,29 +1008,28 @@
     if (error) {
         g_task_return_error (task, error);
         g_object_unref (task);
+        goto out;
     }
+
     /* Need to retry? */
-    else if (ready_state == MBIM_SUBSCRIBER_READY_STATE_NOT_INITIALIZED ||
-             ready_state == MBIM_SUBSCRIBER_READY_STATE_SIM_NOT_INSERTED) {
-        if (--ctx->n_ready_status_checks == 0) {
-            /* All retries consumed, issue error */
+    if (ready_state == MBIM_SUBSCRIBER_READY_STATE_NOT_INITIALIZED ||
+        ready_state == MBIM_SUBSCRIBER_READY_STATE_SIM_NOT_INSERTED) {
+        /* All retries consumed? issue error */
+        if (ctx->last_attempt) {
             if (ready_state == MBIM_SUBSCRIBER_READY_STATE_SIM_NOT_INSERTED)
-                g_task_return_error (
-                    task,
-                    mm_mobile_equipment_error_for_code (MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED));
+                g_task_return_error (task, mm_mobile_equipment_error_for_code (MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED));
             else
-                g_task_return_new_error (task,
-                                         MM_CORE_ERROR,
-                                         MM_CORE_ERROR_FAILED,
+                g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                          "Error waiting for SIM to get initialized");
-            g_object_unref (task);
-        } else {
-            /* Retry */
-            g_timeout_add_seconds (1, (GSourceFunc)wait_for_sim_ready, task);
-        }
+        } else
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_RETRY,
+                                     "SIM not ready yet (retry)");
+        g_object_unref (task);
+        goto out;
     }
+
     /* Initialized but locked? */
-    else if (ready_state == MBIM_SUBSCRIBER_READY_STATE_DEVICE_LOCKED) {
+    if (ready_state == MBIM_SUBSCRIBER_READY_STATE_DEVICE_LOCKED) {
         MbimMessage *message;
 
         /* Query which lock is to unlock */
@@ -1042,14 +1041,19 @@
                              (GAsyncReadyCallback)pin_query_ready,
                              task);
         mbim_message_unref (message);
+        goto out;
     }
-    /* Initialized but locked? */
-    else if (ready_state == MBIM_SUBSCRIBER_READY_STATE_INITIALIZED) {
+
+    /* Initialized! */
+    if (ready_state == MBIM_SUBSCRIBER_READY_STATE_INITIALIZED) {
         g_task_return_boolean (task, TRUE);
         g_object_unref (task);
-    } else
-        g_assert_not_reached ();
+        goto out;
+    }
 
+    g_assert_not_reached ();
+
+out:
     if (response)
         mbim_message_unref (response);
 }
@@ -1074,6 +1078,7 @@
 
 static void
 modem_load_unlock_required (MMIfaceModem *self,
+                            gboolean last_attempt,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
 {
@@ -1086,7 +1091,7 @@
 
     ctx = g_slice_new (LoadUnlockRequiredContext);
     ctx->device = g_object_ref (device);
-    ctx->n_ready_status_checks = 10;
+    ctx->last_attempt = last_attempt;
 
     task = g_task_new (self, NULL, callback, user_data);
     g_task_set_task_data (task, ctx, (GDestroyNotify)load_unlock_required_context_free);
@@ -3938,24 +3943,41 @@
 {
     MbimMessage *response;
     GError *error = NULL;
-    MbimNwError nw_error;
 
     response = mbim_device_command_finish (device, res, &error);
+    /* According to Mobile Broadband Interface Model specification 1.0,
+     * Errata 1, table 10.5.9.8: Status codes for MBIM_CID_REGISTER_STATE,
+     * NwError field of MBIM_REGISTRATION_STATE_INFO structure is valid
+     * if and only if MBIM_SET_REGISTRATION_STATE response status code equals
+     * MBIM_STATUS_FAILURE.
+     * Therefore it only makes sense to parse this value if MBIM_STATUS_FAILURE
+     * result is returned in response, contrary to usual "success" code.
+     * However, some modems do not set this value to 0 when registered,
+     * causing ModemManager to drop to idle state, while modem itself is
+     * registered.
+     * Also NwError "0" is defined in 3GPP TS 24.008 as "Unknown error",
+     * not "No error", making it unsuitable as condition for registration check.
+     */
     if (response &&
-        mbim_message_response_get_result (response, MBIM_MESSAGE_TYPE_COMMAND_DONE, &error) &&
-        mbim_message_register_state_response_parse (
-            response,
-            &nw_error,
-            NULL, /* &register_state */
-            NULL, /* register_mode */
-            NULL, /* available_data_classes */
-            NULL, /* current_cellular_class */
-            NULL, /* provider_id */
-            NULL, /* provider_name */
-            NULL, /* roaming_text */
-            NULL, /* registration_flag */
-            NULL)) {
-        if (nw_error)
+        !mbim_message_response_get_result (response,
+                                           MBIM_MESSAGE_TYPE_COMMAND_DONE,
+                                           &error) &&
+        g_error_matches (error, MBIM_STATUS_ERROR, MBIM_STATUS_ERROR_FAILURE)) {
+        MbimNwError nw_error;
+
+        g_clear_error (&error);
+        if (mbim_message_register_state_response_parse (
+                response,
+                &nw_error,
+                NULL, /* &register_state */
+                NULL, /* register_mode */
+                NULL, /* available_data_classes */
+                NULL, /* current_cellular_class */
+                NULL, /* provider_id */
+                NULL, /* provider_name */
+                NULL, /* roaming_text */
+                NULL, /* registration_flag */
+                &error))
             error = mm_mobile_equipment_error_from_mbim_nw_error (nw_error);
     }
 
@@ -4452,7 +4474,7 @@
 {
     GTask                       *task = NULL;
     MMModem3gppUssdSessionState  ussd_state = MM_MODEM_3GPP_USSD_SESSION_STATE_IDLE;
-    GByteArray                  *bytearray = NULL;
+    GByteArray                  *bytearray;
     gchar                       *converted = NULL;
     GError                      *error = NULL;
 
@@ -4462,8 +4484,9 @@
         self->priv->pending_ussd_action = NULL;
     }
 
-    if (data_size)
-        bytearray = g_byte_array_append (g_byte_array_new (), data, data_size);
+    bytearray = g_byte_array_new ();
+    if (data && data_size)
+        bytearray = g_byte_array_append (bytearray, data, data_size);
 
     switch (ussd_response) {
     case MBIM_USSD_RESPONSE_NO_ACTION_REQUIRED:
@@ -4520,8 +4543,7 @@
 
     mm_iface_modem_3gpp_ussd_update_state (MM_IFACE_MODEM_3GPP_USSD (self), ussd_state);
 
-    if (bytearray)
-        g_byte_array_unref (bytearray);
+    g_byte_array_unref (bytearray);
 
     /* Complete the pending action */
     if (task) {
diff --git a/src/mm-broadband-modem-qmi.c b/src/mm-broadband-modem-qmi.c
index 7356a5e..6329c21 100644
--- a/src/mm-broadband-modem-qmi.c
+++ b/src/mm-broadband-modem-qmi.c
@@ -648,6 +648,7 @@
     LoadUnlockRequiredStep step;
     QmiClient *dms;
     QmiClient *uim;
+    gboolean last_attempt;
 } LoadUnlockRequiredContext;
 
 static MMModemLock
@@ -903,10 +904,13 @@
                                            GAsyncResult *res,
                                            GTask *task)
 {
+    LoadUnlockRequiredContext *ctx;
     QmiMessageUimGetCardStatusOutput *output;
     GError *error = NULL;
     MMModemLock lock = MM_MODEM_LOCK_UNKNOWN;
 
+    ctx = g_task_get_task_data (task);
+
     output = qmi_client_uim_get_card_status_finish (client, res, &error);
     if (!output) {
         g_prefix_error (&error, "QMI operation failed: ");
@@ -919,6 +923,15 @@
                                            &lock,
                                            NULL, NULL, NULL, NULL,
                                            &error)) {
+        /* The device may report a SIM NOT INSERTED error if we're querying the
+         * card status soon after power on. We'll let the Modem interface generic
+         * logic retry loading the info a bit later if that's the case. This will
+         * make device detection slower when there's really no SIM card, but there's
+         * no clear better way to handle it :/ */
+        if (g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED) && !ctx->last_attempt) {
+            g_clear_error (&error);
+            g_set_error (&error, MM_CORE_ERROR, MM_CORE_ERROR_RETRY, "No card found (retry)");
+        }
         g_prefix_error (&error, "QMI operation failed: ");
         g_task_return_error (task, error);
     } else
@@ -1101,6 +1114,7 @@
 
 static void
 modem_load_unlock_required (MMIfaceModem *self,
+                            gboolean last_attempt,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
 {
@@ -1109,6 +1123,7 @@
 
     ctx = g_new0 (LoadUnlockRequiredContext, 1);
     ctx->step = LOAD_UNLOCK_REQUIRED_STEP_FIRST;
+    ctx->last_attempt = last_attempt;
 
     task = g_task_new (self, NULL, callback, user_data);
     g_task_set_task_data (task, ctx, g_free);
diff --git a/src/mm-broadband-modem.c b/src/mm-broadband-modem.c
index 0a51b4b..3cbf513 100644
--- a/src/mm-broadband-modem.c
+++ b/src/mm-broadband-modem.c
@@ -13,7 +13,8 @@
  * Copyright (C) 2008 - 2009 Novell, Inc.
  * Copyright (C) 2009 - 2012 Red Hat, Inc.
  * Copyright (C) 2011 - 2012 Google, Inc.
- * Copyright (C) 2015 - Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #include <config.h>
@@ -120,6 +121,7 @@
     PROP_MODEM_SIM_HOT_SWAP_SUPPORTED,
     PROP_MODEM_SIM_HOT_SWAP_CONFIGURED,
     PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED,
+    PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED,
     PROP_MODEM_CARRIER_CONFIG_MAPPING,
     PROP_FLOW_CONTROL,
     PROP_LAST
@@ -223,8 +225,10 @@
 
     /*<--- Modem Voice interface --->*/
     /* Properties */
-    GObject *modem_voice_dbus_skeleton;
+    GObject    *modem_voice_dbus_skeleton;
     MMCallList *modem_voice_call_list;
+    gboolean    periodic_call_list_check_disabled;
+    gboolean    clcc_supported;
 
     /*<--- Modem Time interface --->*/
     /* Properties */
@@ -1316,6 +1320,7 @@
 
 static void
 modem_load_unlock_required (MMIfaceModem *self,
+                            gboolean last_attempt,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
 {
@@ -1334,7 +1339,7 @@
     mm_dbg ("checking if unlock required...");
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               "+CPIN?",
-                              3,
+                              10,
                               FALSE,
                               (GAsyncReadyCallback)cpin_query_ready,
                               task);
@@ -1957,8 +1962,8 @@
  * try the other command if the first one fails.
  */
 static const MMBaseModemAtCommand signal_quality_csq_sequence[] = {
-    { "+CSQ",  3, TRUE, response_processor_string_ignore_at_errors },
-    { "+CSQ?", 3, TRUE, response_processor_string_ignore_at_errors },
+    { "+CSQ",  3, FALSE, response_processor_string_ignore_at_errors },
+    { "+CSQ?", 3, FALSE, response_processor_string_ignore_at_errors },
     { NULL }
 };
 
@@ -3312,28 +3317,28 @@
 
     /* CMER on primary port */
     if (!ctx->cmer_primary_done && ctx->cmer_command && ctx->primary) {
-        mm_dbg ("Enabling +CIND event reporting in primary port...");
+        mm_dbg ("%s +CIND event reporting in primary port...", ctx->enable ? "Enabling" : "Disabling");
         ctx->cmer_primary_done = TRUE;
         command = ctx->cmer_command;
         port = ctx->primary;
     }
     /* CMER on secondary port */
     else if (!ctx->cmer_secondary_done && ctx->cmer_command && ctx->secondary) {
-        mm_dbg ("Enabling +CIND event reporting in secondary port...");
+        mm_dbg ("%s +CIND event reporting in secondary port...", ctx->enable ? "Enabling" : "Disabling");
         ctx->cmer_secondary_done = TRUE;
         command = ctx->cmer_command;
         port = ctx->secondary;
     }
     /* CGEREP on primary port */
     else if (!ctx->cgerep_primary_done && ctx->cgerep_command && ctx->primary) {
-        mm_dbg ("Enabling +CGEV event reporting in primary port...");
+        mm_dbg ("%s +CGEV event reporting in primary port...", ctx->enable ? "Enabling" : "Disabling");
         ctx->cgerep_primary_done = TRUE;
         command = ctx->cgerep_command;
         port = ctx->primary;
     }
     /* CGEREP on secondary port */
     else if (!ctx->cgerep_secondary_done && ctx->cgerep_command && ctx->secondary) {
-        mm_dbg ("Enabling +CGEV event reporting in secondary port...");
+        mm_dbg ("%s +CGEV event reporting in secondary port...", ctx->enable ? "Enabling" : "Disabling");
         ctx->cgerep_secondary_done = TRUE;
         port = ctx->secondary;
         command = ctx->cgerep_command;
@@ -6959,7 +6964,7 @@
                      0, 0, NULL);
     g_assert (r);
 
-    if (!g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, NULL)) {
+    if (!g_regex_match (r, response, 0, &match_info)) {
         g_task_return_new_error (task,
                                  MM_CORE_ERROR,
                                  MM_CORE_ERROR_INVALID_ARGS,
@@ -7200,6 +7205,24 @@
 }
 
 static void
+clcc_format_check_ready (MMBroadbandModem *self,
+                         GAsyncResult     *res,
+                         GTask            *task)
+{
+    /* +CLCC supported unless we got any error response */
+    self->priv->clcc_supported = !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, NULL);
+
+    /* If +CLCC unsupported we disable polling in the parent directly */
+    g_object_set (self,
+                  MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED, !self->priv->clcc_supported,
+                  NULL);
+
+    /* ATH command is supported; assume we have full voice capabilities */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
 ath_format_check_ready (MMBroadbandModem *self,
                         GAsyncResult *res,
                         GTask *task)
@@ -7213,9 +7236,13 @@
         return;
     }
 
-    /* ATH command is supported; assume we have full voice capabilities */
-    g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
+    /* Also check if +CLCC is supported */
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CLCC=?",
+                              3,
+                              TRUE,
+                              (GAsyncReadyCallback)clcc_format_check_ready,
+                              task);
 }
 
 static void
@@ -7239,6 +7266,149 @@
 }
 
 /*****************************************************************************/
+/* Load full list of calls (Voice interface) */
+
+static gboolean
+modem_voice_load_call_list_finish (MMIfaceModemVoice  *self,
+                                   GAsyncResult       *res,
+                                   GList             **out_call_info_list,
+                                   GError            **error)
+{
+    GList  *call_info_list;
+    GError *inner_error = NULL;
+
+    call_info_list = g_task_propagate_pointer (G_TASK (res), &inner_error);
+    if (inner_error) {
+        g_assert (!call_info_list);
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    *out_call_info_list = call_info_list;
+    return TRUE;
+}
+
+static void
+clcc_ready (MMBaseModem  *modem,
+            GAsyncResult *res,
+            GTask        *task)
+{
+    const gchar *response;
+    GError      *error = NULL;
+    GList       *call_info_list = NULL;
+
+    response = mm_base_modem_at_command_finish (modem, res, &error);
+    if (!response || !mm_3gpp_parse_clcc_response (response, &call_info_list, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_pointer (task, call_info_list, (GDestroyNotify)mm_3gpp_call_info_list_free);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_load_call_list (MMIfaceModemVoice   *self,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+    GTask *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CLCC",
+                              5,
+                              FALSE,
+                              (GAsyncReadyCallback)clcc_ready,
+                              task);
+}
+
+/*****************************************************************************/
+/* Setup/cleanup voice related in-call unsolicited events (Voice interface) */
+
+static gboolean
+modem_voice_setup_cleanup_in_call_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                             GAsyncResult       *res,
+                                                             GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+in_call_event_received (MMPortSerialAt   *port,
+                        GMatchInfo       *info,
+                        MMBroadbandModem *self)
+{
+    MMCallInfo  call_info;
+    gchar      *str;
+
+    call_info.index     = 0;
+    call_info.direction = MM_CALL_DIRECTION_UNKNOWN;
+    call_info.state     = MM_CALL_STATE_TERMINATED;
+    call_info.number    = NULL;
+
+    str = g_match_info_fetch (info, 1);
+    mm_dbg ("Call terminated: %s", str);
+    g_free (str);
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+set_voice_in_call_unsolicited_events_handlers (MMIfaceModemVoice   *self,
+                                               gboolean             enable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+    MMPortSerialAt *ports[2];
+    GRegex         *in_call_event_regex;
+    guint           i;
+    GTask          *task;
+
+    in_call_event_regex = g_regex_new ("\\r\\n(NO CARRIER|BUSY|NO ANSWER|NO DIALTONE)\\r\\n$",
+                                        G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+
+    ports[0] = mm_base_modem_peek_port_primary (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    /* Enable unsolicited events in given port */
+    for (i = 0; i < 2; i++) {
+        if (!ports[i])
+            continue;
+
+        mm_dbg ("(%s) %s voice in-call unsolicited events handlers",
+                mm_port_get_device (MM_PORT (ports[i])),
+                enable ? "Setting" : "Removing");
+        mm_port_serial_at_add_unsolicited_msg_handler (
+            ports[i],
+            in_call_event_regex,
+            enable ? (MMPortSerialAtUnsolicitedMsgFn) in_call_event_received : NULL,
+            enable ? self : NULL,
+            NULL);
+    }
+
+    g_regex_unref (in_call_event_regex);
+
+    task = g_task_new (self, NULL, callback, user_data);
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_setup_in_call_unsolicited_events (MMIfaceModemVoice   *self,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+    set_voice_in_call_unsolicited_events_handlers (self, TRUE, callback, user_data);
+}
+
+static void
+modem_voice_cleanup_in_call_unsolicited_events (MMIfaceModemVoice   *self,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+    set_voice_in_call_unsolicited_events_handlers (self, FALSE, callback, user_data);
+}
+
+/*****************************************************************************/
 /* Setup/cleanup voice related unsolicited events (Voice interface) */
 
 static gboolean
@@ -7250,40 +7420,76 @@
 }
 
 static void
-ring_received (MMPortSerialAt *port,
-               GMatchInfo *info,
+ccwa_received (MMPortSerialAt   *port,
+               GMatchInfo       *info,
                MMBroadbandModem *self)
 {
-    mm_dbg ("Ringing");
-    mm_iface_modem_voice_report_incoming_call (MM_IFACE_MODEM_VOICE (self), NULL);
+    MMCallInfo call_info;
+
+    call_info.index     = 0;
+    call_info.direction = MM_CALL_DIRECTION_INCOMING;
+    call_info.state     = MM_CALL_STATE_WAITING;
+    call_info.number    = mm_get_string_unquoted_from_match_info (info, 1);
+
+    mm_dbg ("Call waiting (%s)", call_info.number);
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+
+    g_free (call_info.number);
 }
 
 static void
-cring_received (MMPortSerialAt *port,
-                GMatchInfo *info,
+ring_received (MMPortSerialAt   *port,
+               GMatchInfo       *info,
+               MMBroadbandModem *self)
+{
+    MMCallInfo call_info;
+
+    call_info.index     = 0;
+    call_info.direction = MM_CALL_DIRECTION_INCOMING;
+    call_info.state     = MM_CALL_STATE_RINGING_IN;
+    call_info.number    = NULL;
+
+    mm_dbg ("Ringing");
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+}
+
+static void
+cring_received (MMPortSerialAt   *port,
+                GMatchInfo       *info,
                 MMBroadbandModem *self)
 {
-    gchar *str;
+    MMCallInfo  call_info;
+    gchar      *str;
 
     /* We could have "VOICE" or "DATA". Now consider only "VOICE" */
-
     str = mm_get_string_unquoted_from_match_info (info, 1);
     mm_dbg ("Ringing (%s)", str);
     g_free (str);
 
-    mm_iface_modem_voice_report_incoming_call (MM_IFACE_MODEM_VOICE (self), NULL);
+    call_info.index     = 0;
+    call_info.direction = MM_CALL_DIRECTION_INCOMING;
+    call_info.state     = MM_CALL_STATE_RINGING_IN;
+    call_info.number    = NULL;
+
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
 }
 
 static void
-clip_received (MMPortSerialAt *port,
-               GMatchInfo *info,
+clip_received (MMPortSerialAt   *port,
+               GMatchInfo       *info,
                MMBroadbandModem *self)
 {
-    gchar *str;
+    MMCallInfo call_info;
 
-    str = mm_get_string_unquoted_from_match_info (info, 1);
-    mm_iface_modem_voice_report_incoming_call (MM_IFACE_MODEM_VOICE (self), str);
-    g_free (str);
+    call_info.index     = 0;
+    call_info.direction = MM_CALL_DIRECTION_INCOMING;
+    call_info.state     = MM_CALL_STATE_RINGING_IN;
+    call_info.number    = mm_get_string_unquoted_from_match_info (info, 1);
+
+    mm_dbg ("Ringing (%s)", call_info.number);
+    mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (self), &call_info);
+
+    g_free (call_info.number);
 }
 
 static void
@@ -7296,12 +7502,14 @@
     GRegex *cring_regex;
     GRegex *ring_regex;
     GRegex *clip_regex;
+    GRegex *ccwa_regex;
     guint i;
     GTask *task;
 
     cring_regex = mm_voice_cring_regex_get ();
     ring_regex  = mm_voice_ring_regex_get ();
     clip_regex  = mm_voice_clip_regex_get ();
+    ccwa_regex  = mm_voice_ccwa_regex_get ();
     ports[0] = mm_base_modem_peek_port_primary (MM_BASE_MODEM (self));
     ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
 
@@ -7332,8 +7540,15 @@
             enable ? (MMPortSerialAtUnsolicitedMsgFn) clip_received : NULL,
             enable ? self : NULL,
             NULL);
+        mm_port_serial_at_add_unsolicited_msg_handler (
+            ports[i],
+            ccwa_regex,
+            enable ? (MMPortSerialAtUnsolicitedMsgFn) ccwa_received : NULL,
+            enable ? self : NULL,
+            NULL);
     }
 
+    g_regex_unref (ccwa_regex);
     g_regex_unref (clip_regex);
     g_regex_unref (cring_regex);
     g_regex_unref (ring_regex);
@@ -7362,77 +7577,452 @@
 /*****************************************************************************/
 /* Enable unsolicited events (CALL indications) (Voice interface) */
 
-static gboolean
-modem_voice_enable_unsolicited_events_finish (MMIfaceModemVoice *self,
-                                              GAsyncResult *res,
-                                              GError **error)
-{
-    GError *inner_error = NULL;
-
-    mm_base_modem_at_sequence_finish (MM_BASE_MODEM (self), res, NULL, &inner_error);
-    if (inner_error) {
-        g_propagate_error (error, inner_error);
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
-static gboolean
-ring_response_processor (MMBaseModem *self,
-                         gpointer none,
-                         const gchar *command,
-                         const gchar *response,
-                         gboolean last_command,
-                         const GError *error,
-                         GVariant **result,
-                         GError **result_error)
-{
-    if (error) {
-        /* If we get a not-supported error and we're not in the last command, we
-         * won't set 'result_error', so we'll keep on the sequence */
-        if (!g_error_matches (error, MM_MESSAGE_ERROR, MM_MESSAGE_ERROR_NOT_SUPPORTED) ||
-            last_command)
-            *result_error = g_error_copy (error);
-
-        return TRUE;
-    }
-
-    *result = NULL;
-    return FALSE;
-}
-
-static const MMBaseModemAtCommand ring_sequence[] = {
-    /* Show caller number on RING. */
-    { "+CLIP=1", 3, FALSE, ring_response_processor },
-    /* Show difference between data call and voice call */
-    { "+CRC=1", 3, FALSE, ring_response_processor },
-    { NULL }
-};
+typedef struct {
+    gboolean        enable;
+    MMPortSerialAt *primary;
+    MMPortSerialAt *secondary;
+    gchar          *clip_command;
+    gboolean        clip_primary_done;
+    gboolean        clip_secondary_done;
+    gchar          *crc_command;
+    gboolean        crc_primary_done;
+    gboolean        crc_secondary_done;
+    gchar          *ccwa_command;
+    gboolean        ccwa_primary_done;
+    gboolean        ccwa_secondary_done;
+} VoiceUnsolicitedEventsContext;
 
 static void
-modem_voice_enable_unsolicited_events (MMIfaceModemVoice *self,
-                                       GAsyncReadyCallback callback,
-                                       gpointer user_data)
+voice_unsolicited_events_context_free (VoiceUnsolicitedEventsContext *ctx)
 {
-    mm_base_modem_at_sequence (
-        MM_BASE_MODEM (self),
-        ring_sequence,
-        NULL, /* response_processor_context */
-        NULL, /* response_processor_context_free */
-        callback,
-        user_data);
+    g_clear_object (&ctx->secondary);
+    g_clear_object (&ctx->primary);
+    g_free (ctx->clip_command);
+    g_free (ctx->crc_command);
+    g_free (ctx->ccwa_command);
+    g_slice_free (VoiceUnsolicitedEventsContext, ctx);
+}
+
+static gboolean
+modem_voice_enable_disable_unsolicited_events_finish (MMIfaceModemVoice  *self,
+                                                      GAsyncResult       *res,
+                                                      GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void run_voice_unsolicited_events_setup (GTask *task);
+
+static void
+voice_unsolicited_events_setup_ready (MMBroadbandModem *self,
+                                      GAsyncResult     *res,
+                                      GTask            *task)
+{
+    VoiceUnsolicitedEventsContext *ctx;
+    GError                        *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, &error)) {
+        mm_dbg ("Couldn't %s voice event reporting: '%s'",
+                ctx->enable ? "enable" : "disable",
+                error->message);
+        g_error_free (error);
+    }
+
+    /* Continue on next port/command */
+    run_voice_unsolicited_events_setup (task);
+}
+
+static void
+run_voice_unsolicited_events_setup (GTask *task)
+{
+    MMBroadbandModem              *self;
+    VoiceUnsolicitedEventsContext *ctx;
+    MMPortSerialAt                *port = NULL;
+    const gchar                   *command = NULL;
+
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
+
+    /* CLIP on primary port */
+    if (!ctx->clip_primary_done && ctx->clip_command && ctx->primary) {
+        mm_dbg ("%s +CLIP calling line reporting in primary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->clip_primary_done = TRUE;
+        command = ctx->clip_command;
+        port = ctx->primary;
+    }
+    /* CLIP on secondary port */
+    else if (!ctx->clip_secondary_done && ctx->clip_command && ctx->secondary) {
+        mm_dbg ("%s +CLIP calling line reporting in primary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->clip_secondary_done = TRUE;
+        command = ctx->clip_command;
+        port = ctx->secondary;
+    }
+    /* CRC on primary port */
+    else if (!ctx->crc_primary_done && ctx->crc_command && ctx->primary) {
+        mm_dbg ("%s +CRC extended format of incoming call indications in primary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->crc_primary_done = TRUE;
+        command = ctx->crc_command;
+        port = ctx->primary;
+    }
+    /* CRC on secondary port */
+    else if (!ctx->crc_secondary_done && ctx->crc_command && ctx->secondary) {
+        mm_dbg ("%s +CRC extended format of incoming call indications in secondary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->crc_secondary_done = TRUE;
+        command = ctx->crc_command;
+        port = ctx->secondary;
+    }
+    /* CCWA on primary port */
+    else if (!ctx->ccwa_primary_done && ctx->ccwa_command && ctx->primary) {
+        mm_dbg ("%s +CCWA call waiting indications in primary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->ccwa_primary_done = TRUE;
+        command = ctx->ccwa_command;
+        port = ctx->primary;
+    }
+    /* CCWA on secondary port */
+    else if (!ctx->ccwa_secondary_done && ctx->ccwa_command && ctx->secondary) {
+        mm_dbg ("%s +CCWA call waiting indications in secondary port...", ctx->enable ? "Enabling" : "Disabling");
+        ctx->ccwa_secondary_done = TRUE;
+        command = ctx->ccwa_command;
+        port = ctx->secondary;
+    }
+
+    /* Enable/Disable unsolicited events in given port */
+    if (port && command) {
+        mm_base_modem_at_command_full (MM_BASE_MODEM (self),
+                                       port,
+                                       command,
+                                       3,
+                                       FALSE,
+                                       FALSE, /* raw */
+                                       NULL, /* cancellable */
+                                       (GAsyncReadyCallback)voice_unsolicited_events_setup_ready,
+                                       task);
+        return;
+    }
+
+    /* Fully done now */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+
+}
+
+static void
+modem_voice_enable_unsolicited_events (MMIfaceModemVoice   *self,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+    VoiceUnsolicitedEventsContext *ctx;
+    GTask                         *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    ctx = g_slice_new0 (VoiceUnsolicitedEventsContext);
+    ctx->enable = TRUE;
+    ctx->primary = mm_base_modem_get_port_primary (MM_BASE_MODEM (self));
+    ctx->secondary = mm_base_modem_get_port_secondary (MM_BASE_MODEM (self));
+
+    /* enable +CLIP URCs with calling line identity */
+    ctx->clip_command = g_strdup ("+CLIP=1");
+    /* enable +CRING URCs instead of plain RING */
+    ctx->crc_command = g_strdup ("+CRC=1");
+    /* enable +CCWA call waiting indications */
+    ctx->ccwa_command = g_strdup ("+CCWA=1");
+
+    g_task_set_task_data (task, ctx, (GDestroyNotify) voice_unsolicited_events_context_free);
+
+    run_voice_unsolicited_events_setup (task);
+}
+
+static void
+modem_voice_disable_unsolicited_events (MMIfaceModemVoice   *self,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+    VoiceUnsolicitedEventsContext *ctx;
+    GTask                         *task;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    ctx = g_slice_new0 (VoiceUnsolicitedEventsContext);
+    ctx->primary = mm_base_modem_get_port_primary (MM_BASE_MODEM (self));
+    ctx->secondary = mm_base_modem_get_port_secondary (MM_BASE_MODEM (self));
+
+    /* disable +CLIP URCs with calling line identity */
+    ctx->clip_command = g_strdup ("+CLIP=0");
+    /* disable +CRING URCs instead of plain RING */
+    ctx->crc_command = g_strdup ("+CRC=0");
+    /* disable +CCWA call waiting indications */
+    ctx->ccwa_command = g_strdup ("+CCWA=0");
+
+    g_task_set_task_data (task, ctx, (GDestroyNotify) voice_unsolicited_events_context_free);
+
+    run_voice_unsolicited_events_setup (task);
 }
 
 /*****************************************************************************/
 /* Create CALL (Voice interface) */
 
 static MMBaseCall *
-modem_voice_create_call (MMIfaceModemVoice *self,
+modem_voice_create_call (MMIfaceModemVoice *_self,
                          MMCallDirection    direction,
                          const gchar       *number)
 {
-    return mm_base_call_new (MM_BASE_MODEM (self), direction, number);
+    MMBroadbandModem *self = MM_BROADBAND_MODEM (_self);
+
+    return mm_base_call_new (MM_BASE_MODEM (self),
+                             direction,
+                             number,
+                             /* If +CLCC is supported, we want no incoming timeout.
+                              * Also, we're able to support detailed call state updates without
+                              * additional vendor-specific commands. */
+                             self->priv->clcc_supported,   /* skip incoming timeout */
+                             self->priv->clcc_supported,   /* dialing->ringing supported */
+                             self->priv->clcc_supported);  /* ringing->active supported */
+}
+
+/*****************************************************************************/
+/* Hold and accept (Voice interface) */
+
+static gboolean
+modem_voice_hold_and_accept_finish (MMIfaceModemVoice  *self,
+                                    GAsyncResult       *res,
+                                    GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_hold_and_accept (MMIfaceModemVoice   *self,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CHLD=2",
+                              20,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
+/* Hangup and accept (Voice interface) */
+
+static gboolean
+modem_voice_hangup_and_accept_finish (MMIfaceModemVoice  *self,
+                                      GAsyncResult       *res,
+                                      GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_hangup_and_accept (MMIfaceModemVoice   *self,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CHLD=1",
+                              20,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
+/* Hangup all (Voice interface) */
+
+static gboolean
+modem_voice_hangup_all_finish (MMIfaceModemVoice  *self,
+                               GAsyncResult       *res,
+                               GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_hangup_all (MMIfaceModemVoice   *self,
+                        GAsyncReadyCallback  callback,
+                        gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CHUP",
+                              3,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
+/* Join multiparty (Voice interface) */
+
+static gboolean
+modem_voice_join_multiparty_finish (MMIfaceModemVoice  *self,
+                                    GAsyncResult       *res,
+                                    GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_join_multiparty (MMIfaceModemVoice   *self,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CHLD=3",
+                              20,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
+/* Leave multiparty (Voice interface) */
+
+static gboolean
+modem_voice_leave_multiparty_finish (MMIfaceModemVoice  *self,
+                                     GAsyncResult       *res,
+                                     GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+chld_leave_multiparty_ready (MMBaseModem  *self,
+                             GAsyncResult *res,
+                             GTask        *task)
+{
+    GError *error = NULL;
+
+    if (!mm_base_modem_at_command_finish (self, res, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+modem_voice_leave_multiparty (MMIfaceModemVoice   *self,
+                              MMBaseCall          *call,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+    GTask *task;
+    guint  idx;
+    gchar *cmd;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    idx = mm_base_call_get_index (call);
+    if (!idx) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "unknown call index");
+        g_object_unref (task);
+        return;
+    }
+
+    cmd = g_strdup_printf ("+CHLD=2%u", idx);
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              cmd,
+                              20,
+                              FALSE,
+                              (GAsyncReadyCallback) chld_leave_multiparty_ready,
+                              task);
+    g_free (cmd);
+}
+
+/*****************************************************************************/
+/* Transfer (Voice interface) */
+
+static gboolean
+modem_voice_transfer_finish (MMIfaceModemVoice  *self,
+                             GAsyncResult       *res,
+                             GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_transfer (MMIfaceModemVoice   *self,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CHLD=4",
+                              20,
+                              FALSE,
+                              callback,
+                              user_data);
+}
+
+/*****************************************************************************/
+/* Call waiting setup (Voice interface) */
+
+static gboolean
+modem_voice_call_waiting_setup_finish (MMIfaceModemVoice  *self,
+                                       GAsyncResult       *res,
+                                       GError            **error)
+{
+    return !!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+}
+
+static void
+modem_voice_call_waiting_setup (MMIfaceModemVoice   *self,
+                                gboolean             enable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+    gchar *cmd;
+
+    /* Enabling or disabling the call waiting service will only be allowed when
+     * the modem is registered in the network, and so, CCWA URC handling will
+     * always be setup at this point (as it's part of the modem enabling phase).
+     * So, just enable or disable the service (second field) but leaving URCs
+     * (first field) always enabled. */
+    cmd = g_strdup_printf ("+CCWA=1,%u", enable);
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              cmd,
+                              60,
+                              FALSE,
+                              callback,
+                              user_data);
+    g_free (cmd);
+}
+
+/*****************************************************************************/
+/* Call waiting query (Voice interface) */
+
+static gboolean
+modem_voice_call_waiting_query_finish (MMIfaceModemVoice  *self,
+                                       GAsyncResult       *res,
+                                       gboolean           *status,
+                                       GError            **error)
+{
+    const gchar *response;
+
+    response = mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, error);
+    if (!response)
+        return FALSE;
+
+    return mm_3gpp_parse_ccwa_service_query_response (response, status, error);
+}
+
+static void
+modem_voice_call_waiting_query (MMIfaceModemVoice   *self,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+    /* This operation will only be allowed while enabled, and so, CCWA URC
+     * handling would always be enabled at this point. So, just perform the
+     * query, but leaving URCs enabled either way. */
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+CCWA=1,2",
+                              60,
+                              FALSE,
+                              callback,
+                              user_data);
 }
 
 /*****************************************************************************/
@@ -7806,12 +8396,6 @@
     guint operating_mode;
 } CallManagerStateResults;
 
-typedef struct {
-    MMBroadbandModem *self;
-    GSimpleAsyncResult *result;
-    MMPortSerialQcdm *qcdm;
-} CallManagerStateContext;
-
 static void
 cm_state_cleanup_port (MMPortSerial *port)
 {
@@ -8235,7 +8819,7 @@
     GError *inner_error = NULL;
     gboolean value;
 
-    value = g_task_propagate_boolean (G_TASK (res), error);
+    value = g_task_propagate_boolean (G_TASK (res), &inner_error);
     if (inner_error) {
         g_propagate_error (error, inner_error);
         return FALSE;
@@ -9641,7 +10225,6 @@
     DISABLING_STEP_IFACE_MESSAGING,
     DISABLING_STEP_IFACE_VOICE,
     DISABLING_STEP_IFACE_LOCATION,
-    DISABLING_STEP_IFACE_CONTACTS,
     DISABLING_STEP_IFACE_CDMA,
     DISABLING_STEP_IFACE_3GPP_USSD,
     DISABLING_STEP_IFACE_3GPP,
@@ -9912,10 +10495,6 @@
         /* Fall down to next step */
         ctx->step++;
 
-    case DISABLING_STEP_IFACE_CONTACTS:
-        /* Fall down to next step */
-        ctx->step++;
-
     case DISABLING_STEP_IFACE_CDMA:
         if (ctx->self->priv->modem_cdma_dbus_skeleton) {
             mm_dbg ("Modem has CDMA capabilities, disabling the Modem CDMA interface...");
@@ -10005,7 +10584,6 @@
     ENABLING_STEP_IFACE_3GPP,
     ENABLING_STEP_IFACE_3GPP_USSD,
     ENABLING_STEP_IFACE_CDMA,
-    ENABLING_STEP_IFACE_CONTACTS,
     ENABLING_STEP_IFACE_LOCATION,
     ENABLING_STEP_IFACE_MESSAGING,
     ENABLING_STEP_IFACE_VOICE,
@@ -10229,10 +10807,6 @@
         /* Fall down to next step */
         ctx->step++;
 
-    case ENABLING_STEP_IFACE_CONTACTS:
-        /* Fall down to next step */
-        ctx->step++;
-
     case ENABLING_STEP_IFACE_LOCATION:
         if (ctx->self->priv->modem_location_dbus_skeleton) {
             mm_dbg ("Modem has location capabilities, enabling the Location interface...");
@@ -10415,7 +10989,6 @@
     INITIALIZE_STEP_IFACE_3GPP,
     INITIALIZE_STEP_IFACE_3GPP_USSD,
     INITIALIZE_STEP_IFACE_CDMA,
-    INITIALIZE_STEP_IFACE_CONTACTS,
     INITIALIZE_STEP_IFACE_LOCATION,
     INITIALIZE_STEP_IFACE_MESSAGING,
     INITIALIZE_STEP_IFACE_VOICE,
@@ -10697,10 +11270,6 @@
         /* Fall down to next step */
         ctx->step++;
 
-    case INITIALIZE_STEP_IFACE_CONTACTS:
-        /* Fall down to next step */
-        ctx->step++;
-
     case INITIALIZE_STEP_IFACE_LOCATION:
         /* Initialize the Location interface */
         mm_iface_modem_location_initialize (MM_IFACE_MODEM_LOCATION (ctx->self),
@@ -11191,6 +11760,9 @@
     case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED:
         self->priv->periodic_signal_check_disabled = g_value_get_boolean (value);
         break;
+    case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED:
+        self->priv->periodic_call_list_check_disabled = g_value_get_boolean (value);
+        break;
     case PROP_MODEM_CARRIER_CONFIG_MAPPING:
         self->priv->carrier_config_mapping = g_value_dup_string (value);
         break;
@@ -11314,6 +11886,9 @@
     case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED:
         g_value_set_boolean (value, self->priv->periodic_signal_check_disabled);
         break;
+    case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED:
+        g_value_set_boolean (value, self->priv->periodic_call_list_check_disabled);
+        break;
     case PROP_MODEM_CARRIER_CONFIG_MAPPING:
         g_value_set_string (value, self->priv->carrier_config_mapping);
         break;
@@ -11350,6 +11925,7 @@
     self->priv->current_sms_mem2_storage = MM_SMS_STORAGE_UNKNOWN;
     self->priv->sim_hot_swap_supported = FALSE;
     self->priv->periodic_signal_check_disabled = FALSE;
+    self->priv->periodic_call_list_check_disabled = FALSE;
     self->priv->modem_cmer_enable_mode = MM_3GPP_CMER_MODE_NONE;
     self->priv->modem_cmer_disable_mode = MM_3GPP_CMER_MODE_NONE;
     self->priv->modem_cmer_ind = MM_3GPP_CMER_IND_NONE;
@@ -11648,10 +12224,36 @@
     iface->setup_unsolicited_events = modem_voice_setup_unsolicited_events;
     iface->setup_unsolicited_events_finish = modem_voice_setup_cleanup_unsolicited_events_finish;
     iface->enable_unsolicited_events = modem_voice_enable_unsolicited_events;
-    iface->enable_unsolicited_events_finish = modem_voice_enable_unsolicited_events_finish;
+    iface->enable_unsolicited_events_finish = modem_voice_enable_disable_unsolicited_events_finish;
+    iface->disable_unsolicited_events = modem_voice_disable_unsolicited_events;
+    iface->disable_unsolicited_events_finish = modem_voice_enable_disable_unsolicited_events_finish;
     iface->cleanup_unsolicited_events = modem_voice_cleanup_unsolicited_events;
     iface->cleanup_unsolicited_events_finish = modem_voice_setup_cleanup_unsolicited_events_finish;
+
+    iface->setup_in_call_unsolicited_events = modem_voice_setup_in_call_unsolicited_events;
+    iface->setup_in_call_unsolicited_events_finish = modem_voice_setup_cleanup_in_call_unsolicited_events_finish;
+    iface->cleanup_in_call_unsolicited_events = modem_voice_cleanup_in_call_unsolicited_events;
+    iface->cleanup_in_call_unsolicited_events_finish = modem_voice_setup_cleanup_in_call_unsolicited_events_finish;
+
     iface->create_call = modem_voice_create_call;
+    iface->load_call_list = modem_voice_load_call_list;
+    iface->load_call_list_finish = modem_voice_load_call_list_finish;
+    iface->hold_and_accept = modem_voice_hold_and_accept;
+    iface->hold_and_accept_finish = modem_voice_hold_and_accept_finish;
+    iface->hangup_and_accept = modem_voice_hangup_and_accept;
+    iface->hangup_and_accept_finish = modem_voice_hangup_and_accept_finish;
+    iface->hangup_all = modem_voice_hangup_all;
+    iface->hangup_all_finish = modem_voice_hangup_all_finish;
+    iface->join_multiparty = modem_voice_join_multiparty;
+    iface->join_multiparty_finish = modem_voice_join_multiparty_finish;
+    iface->leave_multiparty = modem_voice_leave_multiparty;
+    iface->leave_multiparty_finish = modem_voice_leave_multiparty_finish;
+    iface->transfer = modem_voice_transfer;
+    iface->transfer_finish = modem_voice_transfer_finish;
+    iface->call_waiting_setup = modem_voice_call_waiting_setup;
+    iface->call_waiting_setup_finish = modem_voice_call_waiting_setup_finish;
+    iface->call_waiting_query = modem_voice_call_waiting_query;
+    iface->call_waiting_query_finish = modem_voice_call_waiting_query_finish;
 }
 
 static void
@@ -11852,6 +12454,10 @@
                                       MM_IFACE_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED);
 
     g_object_class_override_property (object_class,
+                                      PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED,
+                                      MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED);
+
+    g_object_class_override_property (object_class,
                                       PROP_MODEM_CARRIER_CONFIG_MAPPING,
                                       MM_IFACE_MODEM_CARRIER_CONFIG_MAPPING);
 
diff --git a/src/mm-call-list.c b/src/mm-call-list.c
index 8110064..e131d20 100644
--- a/src/mm-call-list.c
+++ b/src/mm-call-list.c
@@ -54,6 +54,20 @@
 
 /*****************************************************************************/
 
+void
+mm_call_list_foreach (MMCallList            *self,
+                      MMCallListForeachFunc  callback,
+                      gpointer               user_data)
+{
+    GList *l;
+
+    g_assert (callback);
+    for (l = self->priv->list; l; l = g_list_next (l))
+        callback (MM_BASE_CALL (l->data), user_data);
+}
+
+/*****************************************************************************/
+
 guint
 mm_call_list_get_count (MMCallList *self)
 {
@@ -84,10 +98,13 @@
 /*****************************************************************************/
 
 MMBaseCall *
-mm_call_list_get_first_ringing_in_call (MMCallList *self)
+mm_call_list_get_first_incoming_call (MMCallList  *self,
+                                      MMCallState  incoming_state)
 {
     GList *l;
 
+    g_assert (incoming_state == MM_CALL_STATE_RINGING_IN || incoming_state == MM_CALL_STATE_WAITING);
+
     for (l = self->priv->list; l; l = g_list_next (l)) {
         MMBaseCall       *call;
         MMCallState       state;
@@ -101,7 +118,7 @@
                       NULL);
 
         if (direction == MM_CALL_DIRECTION_INCOMING &&
-            state     == MM_CALL_STATE_RINGING_IN) {
+            state     == incoming_state) {
             return call;
         }
     }
@@ -118,6 +135,19 @@
     return g_strcmp0 (mm_base_call_get_path (call), path);
 }
 
+MMBaseCall *
+mm_call_list_get_call (MMCallList  *self,
+                       const gchar *call_path)
+{
+    GList *l;
+
+    l = g_list_find_custom (self->priv->list,
+                            (gpointer)call_path,
+                            (GCompareFunc)cmp_call_by_path);
+
+    return (l ? MM_BASE_CALL (l->data) : NULL);
+}
+
 gboolean
 mm_call_list_delete_call (MMCallList   *self,
                           const gchar  *call_path,
diff --git a/src/mm-call-list.h b/src/mm-call-list.h
index 0cf7523..3f85d1d 100644
--- a/src/mm-call-list.h
+++ b/src/mm-call-list.h
@@ -64,10 +64,20 @@
 void mm_call_list_add_call  (MMCallList *self,
                              MMBaseCall *call);
 
+MMBaseCall *mm_call_list_get_call (MMCallList   *self,
+                                   const gchar  *call_path);
+
 gboolean mm_call_list_delete_call (MMCallList   *self,
                                    const gchar  *call_path,
                                    GError      **error);
 
-MMBaseCall *mm_call_list_get_first_ringing_in_call (MMCallList *self);
+MMBaseCall *mm_call_list_get_first_incoming_call (MMCallList  *self,
+                                                  MMCallState  incoming_state);
+
+typedef void (* MMCallListForeachFunc) (MMBaseCall            *call,
+                                        gpointer               user_data);
+void            mm_call_list_foreach   (MMCallList            *self,
+                                        MMCallListForeachFunc  callback,
+                                        gpointer               user_data);
 
 #endif /* MM_CALL_LIST_H */
diff --git a/src/mm-charsets.c b/src/mm-charsets.c
index 73dd4da..023dcf8 100644
--- a/src/mm-charsets.c
+++ b/src/mm-charsets.c
@@ -541,7 +541,7 @@
 
     if (c <= 0x7F)
         return TRUE;
-    for (i = 0; i < sizeof (t) / sizeof (t[0]); i++) {
+    for (i = 0; i < G_N_ELEMENTS (t); i++) {
         if (c == t[i])
             return TRUE;
     }
diff --git a/src/mm-filter.c b/src/mm-filter.c
index e65a628..73c6bde 100644
--- a/src/mm-filter.c
+++ b/src/mm-filter.c
@@ -34,11 +34,50 @@
 };
 
 struct _MMFilterPrivate {
-    MMFilterRule enabled_rules;
+    MMFilterRule  enabled_rules;
+    GList        *plugin_whitelist_tags;
+    GArray       *plugin_whitelist_product_ids;
 };
 
 /*****************************************************************************/
 
+void
+mm_filter_register_plugin_whitelist_tag (MMFilter    *self,
+                                         const gchar *tag)
+{
+    if (!g_list_find_custom (self->priv->plugin_whitelist_tags, tag, (GCompareFunc) g_strcmp0)) {
+        mm_dbg ("[filter] registered plugin whitelist tag: %s", tag);
+        self->priv->plugin_whitelist_tags = g_list_prepend (self->priv->plugin_whitelist_tags, g_strdup (tag));
+    }
+}
+
+void
+mm_filter_register_plugin_whitelist_product_id (MMFilter *self,
+                                                guint16   vid,
+                                                guint16   pid)
+{
+    mm_uint16_pair new_item;
+    guint          i;
+
+    if (!self->priv->plugin_whitelist_product_ids)
+        self->priv->plugin_whitelist_product_ids = g_array_sized_new (FALSE, FALSE, sizeof (mm_uint16_pair), 10);
+
+    for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) {
+        mm_uint16_pair *item;
+
+        item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i);
+        if (item->l == vid && item->r == pid)
+            return;
+    }
+
+    new_item.l = vid;
+    new_item.r = pid;
+    g_array_append_val (self->priv->plugin_whitelist_product_ids, new_item);
+    mm_dbg ("[filter] registered plugin whitelist product id: %04x:%04x", vid, pid);
+}
+
+/*****************************************************************************/
+
 gboolean
 mm_filter_port (MMFilter        *self,
                 MMKernelDevice  *port,
@@ -60,6 +99,46 @@
         return TRUE;
     }
 
+    /* If the device is explicitly blacklisted, we ignore every port. */
+    if ((self->priv->enabled_rules & MM_FILTER_RULE_EXPLICIT_BLACKLIST) &&
+        (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_IGNORE))) {
+        mm_dbg ("[filter] (%s/%s): port filtered: device is blacklisted", subsystem, name);
+        return FALSE;
+    }
+
+    /* If the device is whitelisted by a plugin, we allow it. */
+    if (self->priv->enabled_rules & MM_FILTER_RULE_PLUGIN_WHITELIST) {
+        GList   *l;
+        guint16  vid = 0;
+        guint16  pid = 0;
+
+        for (l = self->priv->plugin_whitelist_tags; l; l = g_list_next (l)) {
+            if (mm_kernel_device_get_global_property_as_boolean (port, (const gchar *)(l->data)) ||
+                mm_kernel_device_get_property_as_boolean (port, (const gchar *)(l->data))) {
+                mm_dbg ("[filter] (%s/%s) port allowed: device is whitelisted by plugin (tag)", subsystem, name);
+                return TRUE;
+            }
+        }
+
+        vid = mm_kernel_device_get_physdev_vid (port);
+        if (vid)
+            pid = mm_kernel_device_get_physdev_pid (port);
+
+        if (vid && pid && self->priv->plugin_whitelist_product_ids) {
+            guint i;
+
+            for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) {
+                mm_uint16_pair *item;
+
+                item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i);
+                if (item->l == vid && item->r == pid) {
+                    mm_dbg ("[filter] (%s/%s) port allowed: device is whitelisted by plugin (vid/pid)", subsystem, name);
+                    return TRUE;
+                }
+            }
+        }
+    }
+
     /* If this is a virtual device, don't allow it */
     if ((self->priv->enabled_rules & MM_FILTER_RULE_VIRTUAL) &&
         (!mm_kernel_device_get_physdev_sysfs_path (port))) {
@@ -92,16 +171,16 @@
 
         /* Ignore blacklisted tty devices. */
         if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_BLACKLIST) &&
-            (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_IGNORE))) {
-            mm_dbg ("[filter] (%s/%s): port filtered: device is blacklisted", subsystem, name);
+            (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_TTY_BLACKLIST))) {
+            mm_dbg ("[filter] (%s/%s): port filtered: tty is blacklisted", subsystem, name);
             return FALSE;
         }
 
         /* Is the device in the manual-only greylist? If so, return if this is an
          * automatic scan. */
         if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY) &&
-            (!manual_scan && mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_MANUAL_SCAN_ONLY))) {
-            mm_dbg ("[filter] (%s/%s): port filtered: device probed only in manual scan", subsystem, name);
+            (!manual_scan && mm_kernel_device_get_global_property_as_boolean (port, ID_MM_TTY_MANUAL_SCAN_ONLY))) {
+            mm_dbg ("[filter] (%s/%s): port filtered: tty probed only in manual scan", subsystem, name);
             return FALSE;
         }
 
@@ -114,12 +193,8 @@
              !g_strcmp0 (physdev_subsystem, "pci") ||
              !g_strcmp0 (physdev_subsystem, "pnp") ||
              !g_strcmp0 (physdev_subsystem, "sdio"))) {
-            if (!mm_kernel_device_get_global_property_as_boolean (port, ID_MM_PLATFORM_DRIVER_PROBE)) {
-                mm_dbg ("[filter] (%s/%s): port filtered: port's parent platform driver is not whitelisted", subsystem, name);
-                return FALSE;
-            }
-            mm_dbg ("[filter] (%s/%s): port allowed: port's parent platform driver is whitelisted", subsystem, name);
-            return TRUE;
+            mm_dbg ("[filter] (%s/%s): port filtered: tty platform driver", subsystem, name);
+            return FALSE;
         }
 
         /* Default allowed? */
@@ -143,7 +218,13 @@
         }
 
         /*
-         * If the TTY kernel driver is cdc-acm and the interface is class=2/subclass=2/protocol=[1-6], allow it.
+         * If the TTY kernel driver is cdc-acm and the interface is not
+         * class=2/subclass=2/protocol=[1-6], forbidden.
+         *
+         * Otherwise, we'll require the modem to have more ports other
+         * than the ttyACM one (see mm_filter_device_and_port()), because
+         * there are lots of Arduino devices out there exposing a single
+         * ttyACM port and wrongly claiming AT protocol support...
          *
          * Class definitions for Communication Devices 1.2
          * Communications Interface Class Control Protocol Codes:
@@ -161,11 +242,12 @@
          */
         if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) &&
             (!g_strcmp0 (driver, "cdc_acm")) &&
-            (mm_kernel_device_get_interface_class (port) == 2) &&
-            (mm_kernel_device_get_interface_subclass (port) == 2) &&
-            (mm_kernel_device_get_interface_protocol (port) >= 1) && (mm_kernel_device_get_interface_protocol (port) <= 6)) {
-            mm_dbg ("[filter] (%s/%s): port allowed: cdc-acm interface reported AT-capable", subsystem, name);
-            return TRUE;
+            ((mm_kernel_device_get_interface_class (port) != 2)    ||
+             (mm_kernel_device_get_interface_subclass (port) != 2) ||
+             (mm_kernel_device_get_interface_protocol (port) < 1)  ||
+             (mm_kernel_device_get_interface_protocol (port) > 6))) {
+            mm_dbg ("[filter] (%s/%s): port filtered: cdc-acm interface is not AT-capable", subsystem, name);
+            return FALSE;
         }
 
         /* Default forbidden? flag the port as maybe-forbidden, and go on */
@@ -184,6 +266,24 @@
 
 /*****************************************************************************/
 
+static gboolean
+device_has_net_port (MMDevice *device)
+{
+    GList *l;
+
+    for (l = mm_device_peek_port_probe_list (device); l; l = g_list_next (l)) {
+        if (!g_strcmp0 (mm_port_probe_get_port_subsys (MM_PORT_PROBE (l->data)), "net"))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+device_has_multiple_ports (MMDevice *device)
+{
+    return (g_list_length (mm_device_peek_port_probe_list (device)) > 1);
+}
+
 gboolean
 mm_filter_device_and_port (MMFilter       *self,
                            MMDevice       *device,
@@ -191,6 +291,7 @@
 {
     const gchar *subsystem;
     const gchar *name;
+    const gchar *driver;
 
     /* If it wasn't flagged as maybe forbidden, there's nothing to do */
     if (!GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (port), FILTER_PORT_MAYBE_FORBIDDEN)))
@@ -200,16 +301,19 @@
     name      = mm_kernel_device_get_name      (port);
 
     /* Check whether this device holds a NET port in addition to this TTY */
-    if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_WITH_NET) {
-        GList *l;
+    if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_WITH_NET) &&
+        device_has_net_port (device)) {
+        mm_dbg ("[filter] (%s/%s): port allowed: device also exports a net interface", subsystem, name);
+        return TRUE;
+    }
 
-        for (l = mm_device_peek_port_probe_list (device); l; l = g_list_next (l)) {
-            if (!g_strcmp0 (mm_port_probe_get_port_subsys (MM_PORT_PROBE (l->data)), "net")) {
-                mm_dbg ("[filter] (%s/%s): port allowed: device also exports a net interface (%s)",
-                        subsystem, name, mm_port_probe_get_port_name (MM_PORT_PROBE (l->data)));
-                return TRUE;
-            }
-        }
+    /* Check whether this device holds any other port in addition to the ttyACM port */
+    driver = mm_kernel_device_get_driver (port);
+    if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) &&
+        (!g_strcmp0 (driver, "cdc_acm")) &&
+        device_has_multiple_ports (device)) {
+        mm_dbg ("[filter] (%s/%s): port allowed: device exports multiple interfaces", subsystem, name);
+        return TRUE;
     }
 
     mm_dbg ("[filter] (%s/%s) port filtered: forbidden", subsystem, name);
@@ -255,6 +359,15 @@
 
 /*****************************************************************************/
 
+gboolean
+mm_filter_check_rule_enabled (MMFilter     *self,
+                              MMFilterRule  rule)
+{
+    return !!(self->priv->enabled_rules & rule);
+}
+
+/*****************************************************************************/
+
 /* If TTY rule enabled, either DEFAULT_ALLOWED or DEFAULT_FORBIDDEN must be set. */
 #define VALIDATE_RULE_TTY(rules) (!(rules & MM_FILTER_RULE_TTY) || \
                                   ((rules & (MM_FILTER_RULE_TTY_DEFAULT_ALLOWED | MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN)) && \
@@ -285,6 +398,8 @@
 
     mm_dbg ("[filter] created");
     mm_dbg ("[filter]   explicit whitelist:         %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_WHITELIST));
+    mm_dbg ("[filter]   explicit blacklist:         %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_BLACKLIST));
+    mm_dbg ("[filter]   plugin whitelist:           %s", RULE_ENABLED_STR (MM_FILTER_RULE_PLUGIN_WHITELIST));
     mm_dbg ("[filter]   virtual devices forbidden:  %s", RULE_ENABLED_STR (MM_FILTER_RULE_VIRTUAL));
     mm_dbg ("[filter]   net devices allowed:        %s", RULE_ENABLED_STR (MM_FILTER_RULE_NET));
     mm_dbg ("[filter]   cdc-wdm devices allowed:    %s", RULE_ENABLED_STR (MM_FILTER_RULE_CDC_WDM));
@@ -353,6 +468,17 @@
 }
 
 static void
+finalize (GObject *object)
+{
+    MMFilter *self = MM_FILTER (object);
+
+    g_clear_pointer (&self->priv->plugin_whitelist_product_ids, g_array_unref);
+    g_list_free_full (self->priv->plugin_whitelist_tags, g_free);
+
+    G_OBJECT_CLASS (mm_filter_parent_class)->finalize (object);
+}
+
+static void
 mm_filter_class_init (MMFilterClass *klass)
 {
     GObjectClass *object_class = G_OBJECT_CLASS (klass);
@@ -362,6 +488,7 @@
     /* Virtual methods */
     object_class->set_property = set_property;
     object_class->get_property = get_property;
+    object_class->finalize     = finalize;
 
     g_object_class_install_property (
         object_class, PROP_ENABLED_RULES,
diff --git a/src/mm-filter.h b/src/mm-filter.h
index 486d65b..0fc4ade 100644
--- a/src/mm-filter.h
+++ b/src/mm-filter.h
@@ -47,22 +47,26 @@
 typedef enum { /*< underscore_name=mm_filter_rule >*/
     MM_FILTER_RULE_NONE                  = 0,
     MM_FILTER_RULE_EXPLICIT_WHITELIST    = 1 << 0,
-    MM_FILTER_RULE_VIRTUAL               = 1 << 1,
-    MM_FILTER_RULE_NET                   = 1 << 2,
-    MM_FILTER_RULE_CDC_WDM               = 1 << 3,
-    MM_FILTER_RULE_TTY                   = 1 << 4,
-    MM_FILTER_RULE_TTY_BLACKLIST         = 1 << 5,
-    MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY  = 1 << 6,
-    MM_FILTER_RULE_TTY_PLATFORM_DRIVER   = 1 << 7,
-    MM_FILTER_RULE_TTY_DEFAULT_ALLOWED   = 1 << 8,
-    MM_FILTER_RULE_TTY_DRIVER            = 1 << 9,
-    MM_FILTER_RULE_TTY_ACM_INTERFACE     = 1 << 10,
-    MM_FILTER_RULE_TTY_WITH_NET          = 1 << 11,
-    MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN = 1 << 12,
+    MM_FILTER_RULE_EXPLICIT_BLACKLIST    = 1 << 1,
+    MM_FILTER_RULE_PLUGIN_WHITELIST      = 1 << 2,
+    MM_FILTER_RULE_VIRTUAL               = 1 << 3,
+    MM_FILTER_RULE_NET                   = 1 << 4,
+    MM_FILTER_RULE_CDC_WDM               = 1 << 5,
+    MM_FILTER_RULE_TTY                   = 1 << 6,
+    MM_FILTER_RULE_TTY_BLACKLIST         = 1 << 7,
+    MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY  = 1 << 8,
+    MM_FILTER_RULE_TTY_PLATFORM_DRIVER   = 1 << 9,
+    MM_FILTER_RULE_TTY_DEFAULT_ALLOWED   = 1 << 10,
+    MM_FILTER_RULE_TTY_DRIVER            = 1 << 11,
+    MM_FILTER_RULE_TTY_ACM_INTERFACE     = 1 << 12,
+    MM_FILTER_RULE_TTY_WITH_NET          = 1 << 13,
+    MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN = 1 << 14,
 } MMFilterRule;
 
 #define MM_FILTER_RULE_ALL                  \
     (MM_FILTER_RULE_EXPLICIT_WHITELIST    | \
+     MM_FILTER_RULE_EXPLICIT_BLACKLIST    | \
+     MM_FILTER_RULE_PLUGIN_WHITELIST      | \
      MM_FILTER_RULE_VIRTUAL               | \
      MM_FILTER_RULE_NET                   | \
      MM_FILTER_RULE_CDC_WDM               | \
@@ -80,6 +84,7 @@
  * device ports unless they're blacklisted in some way or another. */
 #define MM_FILTER_POLICY_DEFAULT           \
     (MM_FILTER_RULE_EXPLICIT_WHITELIST   | \
+     MM_FILTER_RULE_EXPLICIT_BLACKLIST   | \
      MM_FILTER_RULE_VIRTUAL              | \
      MM_FILTER_RULE_NET                  | \
      MM_FILTER_RULE_CDC_WDM              | \
@@ -93,6 +98,8 @@
  * if they are allowed by any of the automatic whitelist rules. */
 #define MM_FILTER_POLICY_STRICT             \
     (MM_FILTER_RULE_EXPLICIT_WHITELIST    | \
+     MM_FILTER_RULE_EXPLICIT_BLACKLIST    | \
+     MM_FILTER_RULE_PLUGIN_WHITELIST      | \
      MM_FILTER_RULE_VIRTUAL               | \
      MM_FILTER_RULE_NET                   | \
      MM_FILTER_RULE_CDC_WDM               | \
@@ -107,6 +114,8 @@
  * blacklists explicitly */
 #define MM_FILTER_POLICY_PARANOID           \
     (MM_FILTER_RULE_EXPLICIT_WHITELIST    | \
+     MM_FILTER_RULE_EXPLICIT_BLACKLIST    | \
+     MM_FILTER_RULE_PLUGIN_WHITELIST      | \
      MM_FILTER_RULE_VIRTUAL               | \
      MM_FILTER_RULE_NET                   | \
      MM_FILTER_RULE_CDC_WDM               | \
@@ -134,4 +143,13 @@
                                     MMDevice       *device,
                                     MMKernelDevice *port);
 
+void     mm_filter_register_plugin_whitelist_tag        (MMFilter    *self,
+                                                         const gchar *tag);
+void     mm_filter_register_plugin_whitelist_product_id (MMFilter    *self,
+                                                         guint16      vid,
+                                                         guint16      pid);
+
+gboolean mm_filter_check_rule_enabled (MMFilter     *self,
+                                       MMFilterRule  rule);
+
 #endif /* MM_FILTER_H */
diff --git a/src/mm-iface-modem-3gpp.c b/src/mm-iface-modem-3gpp.c
index 3577140..16af6af 100644
--- a/src/mm-iface-modem-3gpp.c
+++ b/src/mm-iface-modem-3gpp.c
@@ -115,60 +115,71 @@
     return ctx;
 }
 
-static gboolean
-reg_state_is_registered (MMModem3gppRegistrationState state)
-{
-    return state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||
-        state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||
-        state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_SMS_ONLY ||
-        state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY ||
-        state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_CSFB_NOT_PREFERRED ||
-        state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED;
-}
+#define REG_STATE_IS_REGISTERED(state)                                    \
+    (state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||                    \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||                 \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_SMS_ONLY ||           \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY ||        \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_CSFB_NOT_PREFERRED || \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED)
+
+#define REG_STATE_IS_UNKNOWN_IDLE_DENIED(state)                           \
+    (state == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN ||                 \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||                    \
+     state == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED)
 
 static MMModem3gppRegistrationState
 get_consolidated_reg_state (RegistrationStateContext *ctx)
 {
+    MMModem3gppRegistrationState consolidated = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
+
     /* Some devices (Blackberries for example) will respond to +CGREG, but
      * return ERROR for +CREG, probably because their firmware is just stupid.
      * So here we prefer the +CREG response, but if we never got a successful
      * +CREG response, we'll take +CGREG instead.
      */
     if (ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||
-        ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
-        return ctx->cs;
+        ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING) {
+        consolidated = ctx->cs;
+        goto out;
+    }
     if (ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||
-        ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
-        return ctx->ps;
+        ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING) {
+        consolidated = ctx->ps;
+        goto out;
+    }
     if (ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
-        return ctx->eps;
+        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING) {
+        consolidated = ctx->eps;
+        goto out;
+    }
 
     /* Searching? */
     if (ctx->cs  == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ||
         ctx->ps  == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ||
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING)
-         return MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING;
+        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING) {
+         consolidated = MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING;
+         goto out;
+    }
 
-    /* If one state is DENIED and the others are UNKNOWN, use DENIED */
-    if (ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED &&
-        ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN &&
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN)
-        return ctx->cs;
-    if (ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN &&
-        ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED &&
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN)
-        return ctx->ps;
-    if (ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN &&
-        ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN &&
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED)
-        return ctx->eps;
+    /* If at least one state is DENIED and the others are UNKNOWN or IDLE, use DENIED */
+    if ((ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED ||
+         ctx->ps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED ||
+         ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED) &&
+        REG_STATE_IS_UNKNOWN_IDLE_DENIED (ctx->cs) &&
+        REG_STATE_IS_UNKNOWN_IDLE_DENIED (ctx->ps) &&
+        REG_STATE_IS_UNKNOWN_IDLE_DENIED (ctx->eps)) {
+        consolidated = MM_MODEM_3GPP_REGISTRATION_STATE_DENIED;
+        goto out;
+    }
 
     /* Emergency services? */
     if (ctx->cs  == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY ||
         ctx->ps  == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY ||
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY)
-         return MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY;
+        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY) {
+         consolidated = MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY;
+         goto out;
+    }
 
     /* Support for additional registration states reported when on LTE.
      *
@@ -188,17 +199,26 @@
         ctx->cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED) {
         mm_warn ("3GPP CSFB registration state is consolidated: %s",
                  mm_modem_3gpp_registration_state_get_string (ctx->cs));
-        return ctx->cs;
+        consolidated = ctx->cs;
+        goto out;
     }
 
     /* Idle? */
     if (ctx->cs  == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||
         ctx->ps  == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||
-        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE)
-         return MM_MODEM_3GPP_REGISTRATION_STATE_IDLE;
+        ctx->eps == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE) {
+         consolidated = MM_MODEM_3GPP_REGISTRATION_STATE_IDLE;
+         goto out;
+    }
 
-    /* Just unknown at this point */
-    return MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
+ out:
+    mm_dbg ("building consolidated registration state: cs '%s', ps '%s', eps '%s' --> '%s'",
+            mm_modem_3gpp_registration_state_get_string (ctx->cs),
+            mm_modem_3gpp_registration_state_get_string (ctx->ps),
+            mm_modem_3gpp_registration_state_get_string (ctx->eps),
+            mm_modem_3gpp_registration_state_get_string (consolidated));
+
+    return consolidated;
 }
 
 /*****************************************************************************/
@@ -314,7 +334,7 @@
     }
 
     /* If we got registered, end registration checks */
-    if (reg_state_is_registered (current_registration_state)) {
+    if (REG_STATE_IS_REGISTERED (current_registration_state)) {
         /* Request immediate access tech and signal update: we may have changed
          * from home to roaming or viceversa, both registered states, so there
          * wouldn't be an explicit refresh triggered from the modem interface as
@@ -442,7 +462,7 @@
 
         /* If the modem is already registered and the last time it was asked
          * automatic registration, we're done */
-        if ((current_operator_code || reg_state_is_registered (reg_state)) &&
+        if ((current_operator_code || REG_STATE_IS_REGISTERED (reg_state)) &&
             !registration_state_context->manual_registration) {
             mm_dbg ("Already registered in network '%s',"
                     " automatic registration not launched...",
@@ -497,7 +517,7 @@
 {
     GError *error = NULL;
 
-    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->register_in_network_finish (self, res,&error))
+    if (!mm_iface_modem_3gpp_register_in_network_finish (self, res, &error))
         g_dbus_method_invocation_take_error (ctx->invocation, error);
     else
         mm_gdbus_modem3gpp_complete_register (ctx->skeleton, ctx->invocation);
@@ -1303,7 +1323,7 @@
 
     /* Even if registration state didn't change, report access technology,
      * but only if something valid to report */
-    if (reg_state_is_registered (state) || ctx->reloading_registration_info) {
+    if (REG_STATE_IS_REGISTERED (state) || ctx->reloading_registration_info) {
         if (access_tech != MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN)
             mm_iface_modem_update_access_technologies (MM_IFACE_MODEM (self),
                                                        access_tech,
@@ -1337,7 +1357,7 @@
      * location updates, but only if something valid to report. For the case
      * where we're registering (loading current registration info after a state
      * change to registered), we also allow LAC/CID updates. */
-    if (reg_state_is_registered (state) || ctx->reloading_registration_info) {
+    if (REG_STATE_IS_REGISTERED (state) || ctx->reloading_registration_info) {
         if ((location_area_code > 0 || tracking_area_code > 0) && cell_id > 0)
             mm_iface_modem_location_3gpp_update_lac_tac_ci (MM_IFACE_MODEM_LOCATION (self),
                                                             location_area_code,
@@ -1420,7 +1440,7 @@
     if (new_state == old_state)
         return;
 
-    if (reg_state_is_registered (new_state)) {
+    if (REG_STATE_IS_REGISTERED (new_state)) {
         MMModemState modem_state;
 
         /* If already reloading registration info, skip it */
diff --git a/src/mm-iface-modem-firmware.c b/src/mm-iface-modem-firmware.c
index ecfa3b5..a3e68c5 100644
--- a/src/mm-iface-modem-firmware.c
+++ b/src/mm-iface-modem-firmware.c
@@ -348,7 +348,7 @@
 
     mm_iface_modem_get_carrier_config (MM_IFACE_MODEM (self), &aux, NULL);
 
-    ids = g_ptr_array_new_with_free_func ((GDestroyNotify)g_free);
+    ids = g_ptr_array_new_with_free_func (g_free);
     if (aux) {
         gchar *carrier;
 
diff --git a/src/mm-iface-modem-location.c b/src/mm-iface-modem-location.c
index 2168140..8391506 100644
--- a/src/mm-iface-modem-location.c
+++ b/src/mm-iface-modem-location.c
@@ -11,7 +11,8 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2012 Google, Inc.
- * Copyright (C) 2012 Lanedo GmbH <aleksander@lanedo.com>
+ * Copyright (C) 2012 Lanedo GmbH
+ * Copyright (C) 2012-2019 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include <ModemManager.h>
@@ -140,7 +141,9 @@
                 break;
             case MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED:
                 g_assert_not_reached ();
-            case MM_MODEM_LOCATION_SOURCE_AGPS:
+            case MM_MODEM_LOCATION_SOURCE_AGPS_MSA:
+                g_assert_not_reached ();
+            case MM_MODEM_LOCATION_SOURCE_AGPS_MSB:
                 g_assert_not_reached ();
             default:
                 g_warn_if_reached ();
@@ -506,7 +509,8 @@
             g_clear_object (&ctx->location_cdma_bs);
         break;
     case MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED:
-    case MM_MODEM_LOCATION_SOURCE_AGPS:
+    case MM_MODEM_LOCATION_SOURCE_AGPS_MSA:
+    case MM_MODEM_LOCATION_SOURCE_AGPS_MSB:
         /* Nothing to setup in the context */
     default:
         break;
@@ -622,7 +626,7 @@
         return;
     }
 
-    while (ctx->current <= MM_MODEM_LOCATION_SOURCE_AGPS) {
+    while (ctx->current <= MM_MODEM_LOCATION_SOURCE_LAST) {
         gchar *source_str;
 
         if (ctx->to_enable & ctx->current) {
@@ -720,8 +724,8 @@
     ctx->to_disable = MM_MODEM_LOCATION_SOURCE_NONE;
 
     /* Loop through all known bits in the bitmask to enable/disable specific location sources */
-    for (source = MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI;
-         source <= MM_MODEM_LOCATION_SOURCE_AGPS;
+    for (source = MM_MODEM_LOCATION_SOURCE_FIRST;
+         source <= MM_MODEM_LOCATION_SOURCE_LAST;
          source = source << 1) {
         /* skip unsupported sources */
         if (!(mm_gdbus_modem_location_get_capabilities (ctx->skeleton) & source))
@@ -765,6 +769,21 @@
         return;
     }
 
+    /* MSA A-GPS and MSB A-GPS cannot be set at the same time */
+    if ((ctx->to_enable & MM_MODEM_LOCATION_SOURCE_AGPS_MSA &&
+         currently_enabled & MM_MODEM_LOCATION_SOURCE_AGPS_MSB) ||
+        (ctx->to_enable & MM_MODEM_LOCATION_SOURCE_AGPS_MSB &&
+         currently_enabled & MM_MODEM_LOCATION_SOURCE_AGPS_MSA) ||
+        (ctx->to_enable & MM_MODEM_LOCATION_SOURCE_AGPS_MSA &&
+         ctx->to_enable & MM_MODEM_LOCATION_SOURCE_AGPS_MSB)) {
+        g_task_return_new_error (task,
+                                 MM_CORE_ERROR,
+                                 MM_CORE_ERROR_FAILED,
+                                 "Cannot have both MSA A-GPS and MSB A-GPS enabled at the same time");
+        g_object_unref (task);
+        return;
+    }
+
     if (ctx->to_enable != MM_MODEM_LOCATION_SOURCE_NONE) {
         str = mm_modem_location_source_build_string_from_mask (ctx->to_enable);
         mm_dbg ("Need to enable the following location sources: '%s'", str);
@@ -778,7 +797,7 @@
     }
 
     /* Start enabling/disabling location sources */
-    ctx->current = MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI;
+    ctx->current = MM_MODEM_LOCATION_SOURCE_FIRST;
     setup_gathering_step (task);
 }
 
@@ -982,7 +1001,7 @@
     }
 
     /* If A-GPS is NOT supported, set error */
-    if (!(mm_gdbus_modem_location_get_capabilities (ctx->skeleton) & MM_MODEM_LOCATION_SOURCE_AGPS)) {
+    if (!(mm_gdbus_modem_location_get_capabilities (ctx->skeleton) & (MM_MODEM_LOCATION_SOURCE_AGPS_MSA | MM_MODEM_LOCATION_SOURCE_AGPS_MSB))) {
         g_dbus_method_invocation_return_error (ctx->invocation,
                                                MM_CORE_ERROR,
                                                MM_CORE_ERROR_UNSUPPORTED,
@@ -1496,7 +1515,8 @@
         default_sources &= ~(MM_MODEM_LOCATION_SOURCE_GPS_RAW |
                              MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
                              MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED |
-                             MM_MODEM_LOCATION_SOURCE_AGPS);
+                             MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                             MM_MODEM_LOCATION_SOURCE_AGPS_MSB);
 
         setup_gathering (self,
                          default_sources,
@@ -1723,7 +1743,8 @@
 
     case INITIALIZATION_STEP_SUPL_SERVER:
         /* If the modem supports A-GPS, load SUPL server */
-        if (ctx->capabilities & MM_MODEM_LOCATION_SOURCE_AGPS &&
+        if ((ctx->capabilities & (MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                                  MM_MODEM_LOCATION_SOURCE_AGPS_MSB)) &&
             MM_IFACE_MODEM_LOCATION_GET_INTERFACE (self)->load_supl_server &&
             MM_IFACE_MODEM_LOCATION_GET_INTERFACE (self)->load_supl_server_finish) {
             MM_IFACE_MODEM_LOCATION_GET_INTERFACE (self)->load_supl_server (
@@ -1737,7 +1758,8 @@
 
     case INITIALIZATION_STEP_SUPPORTED_ASSISTANCE_DATA:
         /* If the modem supports any GPS-related technology, check assistance data types supported */
-        if ((ctx->capabilities & (MM_MODEM_LOCATION_SOURCE_AGPS    |
+        if ((ctx->capabilities & (MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                                  MM_MODEM_LOCATION_SOURCE_AGPS_MSB |
                                   MM_MODEM_LOCATION_SOURCE_GPS_RAW |
                                   MM_MODEM_LOCATION_SOURCE_GPS_NMEA)) &&
             MM_IFACE_MODEM_LOCATION_GET_INTERFACE (self)->load_supported_assistance_data &&
diff --git a/src/mm-iface-modem-messaging.c b/src/mm-iface-modem-messaging.c
index 8b3ac8c..fd50f54 100644
--- a/src/mm-iface-modem-messaging.c
+++ b/src/mm-iface-modem-messaging.c
@@ -498,6 +498,8 @@
     paths = mm_sms_list_get_paths (list);
     mm_gdbus_modem_messaging_set_messages (skeleton, (const gchar *const *)paths);
     g_strfreev (paths);
+
+    g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (skeleton));
 }
 
 static void
diff --git a/src/mm-iface-modem-time.c b/src/mm-iface-modem-time.c
index d1a0dfb..b8d35bf 100644
--- a/src/mm-iface-modem-time.c
+++ b/src/mm-iface-modem-time.c
@@ -354,6 +354,27 @@
     g_object_unref (skeleton);
 }
 
+void
+mm_iface_modem_time_update_network_timezone (MMIfaceModemTime  *self,
+                                             MMNetworkTimezone *tz)
+{
+    MmGdbusModemTime *skeleton;
+    GVariant         *dictionary;
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_TIME_DBUS_SKELETON, &skeleton,
+                  NULL);
+    if (!skeleton)
+        return;
+
+    dictionary = mm_network_timezone_get_dictionary (tz);
+    mm_gdbus_modem_time_set_network_timezone (skeleton, dictionary);
+    if (dictionary)
+        g_variant_unref (dictionary);
+
+    g_object_unref (skeleton);
+}
+
 /*****************************************************************************/
 
 typedef struct _DisablingContext DisablingContext;
diff --git a/src/mm-iface-modem-time.h b/src/mm-iface-modem-time.h
index 7712e4c..462d8b9 100644
--- a/src/mm-iface-modem-time.h
+++ b/src/mm-iface-modem-time.h
@@ -130,7 +130,9 @@
 
 /* Implementations of the unsolicited events handling should call this method
  * to notify about the updated time */
-void mm_iface_modem_time_update_network_time (MMIfaceModemTime *self,
-                                              const gchar *network_time);
+void mm_iface_modem_time_update_network_time     (MMIfaceModemTime  *self,
+                                                  const gchar       *network_time);
+void mm_iface_modem_time_update_network_timezone (MMIfaceModemTime  *self,
+                                                  MMNetworkTimezone *tz);
 
 #endif /* MM_IFACE_MODEM_TIME_H */
diff --git a/src/mm-iface-modem-voice.c b/src/mm-iface-modem-voice.c
index e278899..1c7af9c 100644
--- a/src/mm-iface-modem-voice.c
+++ b/src/mm-iface-modem-voice.c
@@ -10,7 +10,8 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2015 - Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #include <ModemManager.h>
@@ -22,11 +23,15 @@
 #include "mm-call-list.h"
 #include "mm-log.h"
 
-#define SUPPORT_CHECKED_TAG "voice-support-checked-tag"
-#define SUPPORTED_TAG       "voice-supported-tag"
+#define SUPPORT_CHECKED_TAG           "voice-support-checked-tag"
+#define SUPPORTED_TAG                 "voice-supported-tag"
+#define CALL_LIST_POLLING_CONTEXT_TAG "voice-call-list-polling-context-tag"
+#define IN_CALL_EVENT_CONTEXT_TAG     "voice-in-call-event-context-tag"
 
 static GQuark support_checked_quark;
 static GQuark supported_quark;
+static GQuark call_list_polling_context_quark;
+static GQuark in_call_event_context_quark;
 
 /*****************************************************************************/
 
@@ -38,13 +43,21 @@
 
 /*****************************************************************************/
 
+/* new calls will inherit audio settings if the modem is already in-call state */
+static void update_audio_settings_in_call (MMIfaceModemVoice *self,
+                                           MMBaseCall        *call);
+
 static MMBaseCall *
 create_incoming_call (MMIfaceModemVoice *self,
                       const gchar       *number)
 {
+    MMBaseCall *call;
+
     g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL);
 
-    return MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number);
+    call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number);
+    update_audio_settings_in_call (self, call);
+    return call;
 }
 
 static MMBaseCall *
@@ -52,6 +65,7 @@
                                       MMCallProperties   *properties,
                                       GError            **error)
 {
+    MMBaseCall  *call;
     const gchar *number;
 
     /* Don't create CALL from properties if either number is missing */
@@ -66,43 +80,180 @@
 
     /* Create a call object as defined by the interface */
     g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL);
-    return MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number);
+    call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number);
+    update_audio_settings_in_call (self, call);
+    return call;
+}
+
+/*****************************************************************************/
+/* Common helper to match call info against a known call object */
+
+static gboolean
+match_single_call_info (const MMCallInfo *call_info,
+                        MMBaseCall       *call)
+{
+    MMCallState      state;
+    MMCallDirection  direction;
+    const gchar     *number;
+    guint            idx;
+    gboolean         match_direction_and_state = FALSE;
+    gboolean         match_index = FALSE;
+    gboolean         match_terminated = FALSE;
+
+    /* try to look for a matching call by direction/number/index */
+    state     = mm_base_call_get_state     (call);
+    direction = mm_base_call_get_direction (call);
+    number    = mm_base_call_get_number    (call);
+    idx       = mm_base_call_get_index     (call);
+
+    /* Match index */
+    if (call_info->index && (call_info->index == idx))
+        match_index = TRUE;
+
+    /* Match direction and state.
+     * We cannot apply this match if both call info and call have an index set
+     * and they're different already. */
+    if ((call_info->direction == direction) &&
+        (call_info->state == state) &&
+        (!call_info->index || !idx || match_index))
+        match_direction_and_state = TRUE;
+
+    /* Match special terminated event.
+     * We cannot apply this match if the call is part of a multiparty
+     * call, because we don't know which of the calls in the multiparty
+     * is the one that finished. Must rely on other reports that do
+     * provide call index. */
+    if ((call_info->state == MM_CALL_STATE_TERMINATED) &&
+        (call_info->direction == MM_CALL_DIRECTION_UNKNOWN) &&
+        !call_info->index &&
+        !call_info->number &&
+        !mm_base_call_get_multiparty (call))
+        match_terminated = TRUE;
+
+    /* If no clear match, nothing to do */
+    if (!match_index && !match_direction_and_state && !match_terminated)
+        return FALSE;
+
+    mm_dbg ("call info matched (matched direction/state %s, matched index %s, matched terminated %s) with call at '%s'",
+            match_direction_and_state ? "yes" : "no",
+            match_index ? "yes" : "no",
+            match_terminated ? "yes" : "no",
+            mm_base_call_get_path (call));
+
+    /* Early detect if a known incoming call that was created
+     * from a plain CRING URC (i.e. without caller number)
+     * needs to have the number provided.
+     */
+    if (call_info->number && !number) {
+        mm_dbg ("  number set: %s", call_info->number);
+        mm_base_call_set_number (call, call_info->number);
+    }
+
+    /* Early detect if a known incoming/outgoing call does
+     * not have a known call index yet.
+     */
+    if (call_info->index && !idx) {
+        mm_dbg ("  index set: %u", call_info->index);
+        mm_base_call_set_index (call, call_info->index);
+    }
+
+    /* Update state if it changed */
+    if (call_info->state != state) {
+        mm_dbg ("  state updated: %s", mm_call_state_get_string (call_info->state));
+        mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_UNKNOWN);
+    }
+
+    /* refresh if incoming and new state is not terminated */
+    if ((call_info->state != MM_CALL_STATE_TERMINATED) &&
+        (direction == MM_CALL_DIRECTION_INCOMING)) {
+        mm_dbg ("  incoming refreshed");
+        mm_base_call_incoming_refresh (call);
+    }
+
+    return TRUE;
 }
 
 /*****************************************************************************/
 
-void
-mm_iface_modem_voice_report_incoming_call (MMIfaceModemVoice *self,
-                                           const gchar       *number)
+typedef struct {
+    const MMCallInfo *call_info;
+} ReportCallForeachContext;
+
+static void
+report_call_foreach (MMBaseCall               *call,
+                     ReportCallForeachContext *ctx)
 {
-    MMBaseCall *call = NULL;
-    MMCallList *list = NULL;
+    /* Do nothing if already matched */
+    if (!ctx->call_info)
+        return;
+
+    /* fully ignore already terminated calls */
+    if (mm_base_call_get_state (call) == MM_CALL_STATE_TERMINATED)
+        return;
+
+    /* Reset call info in context if the call info matches an existing call */
+    if (match_single_call_info (ctx->call_info, call))
+        ctx->call_info = NULL;
+}
+
+void
+mm_iface_modem_voice_report_call (MMIfaceModemVoice *self,
+                                  const MMCallInfo  *call_info)
+{
+    ReportCallForeachContext  ctx = { 0 };
+    MMBaseCall               *call = NULL;
+    MMCallList               *list = NULL;
+
+    /* When reporting single call, the only mandatory parameter is the state:
+     *   - index is optional (e.g. unavailable when receiving +CLIP URCs)
+     *   - number is optional (e.g. unavailable when receiving +CRING URCs)
+     *   - direction is optional (e.g. unavailable when receiving some vendor-specific URCs)
+     */
+    g_assert (call_info->state != MM_CALL_STATE_UNKNOWN);
+
+    /* Early debugging of the call state update */
+    mm_dbg ("call at index %u: direction %s, state %s, number %s",
+            call_info->index,
+            mm_call_direction_get_string (call_info->direction),
+            mm_call_state_get_string (call_info->state),
+            call_info->number ? call_info->number : "n/a");
 
     g_object_get (MM_BASE_MODEM (self),
                   MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                   NULL);
 
     if (!list) {
-        mm_warn ("Cannot create incoming call: missing call list");
+        mm_warn ("Cannot process call state update: missing call list");
         return;
     }
 
-    call = mm_call_list_get_first_ringing_in_call (list);
+    /* Iterate over all known calls and try to match a known one */
+    ctx.call_info = call_info;
+    mm_call_list_foreach (list, (MMCallListForeachFunc)report_call_foreach, &ctx);
 
-    /* If call exists already, refresh its validity and set number if it wasn't set */
-    if (call) {
-        if (number && !mm_gdbus_call_get_number (MM_GDBUS_CALL (call)))
-            mm_gdbus_call_set_number (MM_GDBUS_CALL (call), number);
-        mm_base_call_incoming_refresh (call);
-        g_object_unref (list);
-        return;
+    /* If call info matched with an existing one, the context call info would have been reseted */
+    if (!ctx.call_info)
+        goto out;
+
+    /* If call info didn't match with any known call, it may be because we're being
+     * reported a NEW incoming call. If that's not the case, we'll ignore the report. */
+    if ((call_info->direction != MM_CALL_DIRECTION_INCOMING) ||
+        ((call_info->state != MM_CALL_STATE_WAITING) && (call_info->state != MM_CALL_STATE_RINGING_IN))) {
+        mm_dbg ("unhandled call state update reported: direction: %s, state %s",
+                mm_call_direction_get_string (call_info->direction),
+                mm_call_state_get_string (call_info->state));
+        goto out;
     }
 
     mm_dbg ("Creating new incoming call...");
-    call = create_incoming_call (self, number);
+    call = create_incoming_call (self, call_info->number);
 
-    /* Set the state as ringing in */
-    mm_base_call_change_state (call, MM_CALL_STATE_RINGING_IN, MM_CALL_STATE_REASON_INCOMING_NEW);
+    /* Set the state */
+    mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_INCOMING_NEW);
+
+    /* Set the index, if known */
+    if (call_info->index)
+        mm_base_call_set_index (call, call_info->index);
 
     /* Start its validity timeout */
     mm_base_call_incoming_refresh (call);
@@ -111,6 +262,181 @@
     mm_base_call_export (call);
     mm_call_list_add_call (list, call);
     g_object_unref (call);
+
+ out:
+    g_object_unref (list);
+}
+
+/*****************************************************************************/
+/* Full current call list reporting
+ *
+ * This method receives as input a list with all the currently active calls,
+ * including the specific state they're in.
+ *
+ * This method should:
+ *  - Check whether we're reporting a new call (i.e. not in our internal call
+ *    list yet). We'll create a new call object if so.
+ *  - Check whether any of the known calls has changed state, and if so,
+ *    update it.
+ *  - Check whether any of the known calls is NOT given in the input list of
+ *    call infos, which would mean the call is terminated.
+ */
+
+typedef struct {
+    GList *call_info_list;
+} ReportAllCallsForeachContext;
+
+static void
+report_all_calls_foreach (MMBaseCall                   *call,
+                          ReportAllCallsForeachContext *ctx)
+{
+    GList *l;
+
+    /* fully ignore already terminated calls */
+    if (mm_base_call_get_state (call) == MM_CALL_STATE_TERMINATED)
+        return;
+
+    /* Iterate over the call info list */
+    for (l = ctx->call_info_list; l; l = g_list_next (l)) {
+        MMCallInfo *call_info = (MMCallInfo *)(l->data);
+
+        /* if match found, delete item from list and halt iteration right away */
+        if (match_single_call_info (call_info, call)) {
+            ctx->call_info_list = g_list_delete_link (ctx->call_info_list, l);
+            return;
+        }
+    }
+
+    /* not found in list! this call is now terminated */
+    mm_base_call_change_state (call, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_UNKNOWN);
+}
+
+void
+mm_iface_modem_voice_report_all_calls (MMIfaceModemVoice *self,
+                                       GList             *call_info_list)
+{
+    ReportAllCallsForeachContext  ctx = { 0 };
+    MMCallList                   *list = NULL;
+    GList                        *l;
+
+    /* Early debugging of the full list of calls */
+    mm_dbg ("Reported %u ongoing calls", g_list_length (call_info_list));
+    for (l = call_info_list; l; l = g_list_next (l)) {
+        MMCallInfo *call_info = (MMCallInfo *)(l->data);
+
+        /* When reporting full list of calls, index and state are mandatory */
+        g_assert (call_info->index != 0);
+        g_assert (call_info->state != MM_CALL_STATE_UNKNOWN);
+
+        mm_dbg ("call at index %u: direction %s, state %s, number %s",
+                call_info->index,
+                mm_call_direction_get_string (call_info->direction),
+                mm_call_state_get_string (call_info->state),
+                call_info->number ? call_info->number : "n/a");
+    }
+
+    /* Retrieve list of known calls */
+    g_object_get (MM_BASE_MODEM (self),
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        mm_warn ("Cannot report all calls: missing call list");
+        return;
+    }
+
+    /* Iterate over all the calls already known to us.
+     * Whenever a known call is updated, it will be removed from the call info list */
+    ctx.call_info_list = g_list_copy (call_info_list);
+    mm_call_list_foreach (list, (MMCallListForeachFunc)report_all_calls_foreach, &ctx);
+
+    /* Once processed, the call info list will have all calls that were unknown to
+     * us, i.e. the new calls to create. We really only expect new incoming calls, so
+     * we'll warn if we get any outgoing call reported here. */
+    for (l = ctx.call_info_list; l; l = g_list_next (l)) {
+        MMCallInfo *call_info = (MMCallInfo *)(l->data);
+
+        if (call_info->direction == MM_CALL_DIRECTION_OUTGOING) {
+            mm_warn ("unexpected outgoing call to number '%s' reported in call list: state %s",
+                     call_info->number ? call_info->number : "n/a",
+                     mm_call_state_get_string (call_info->state));
+            continue;
+        }
+
+        if (call_info->direction == MM_CALL_DIRECTION_INCOMING) {
+            MMBaseCall *call;
+
+            /* We only expect either RINGING-IN or WAITING states */
+            if ((call_info->state != MM_CALL_STATE_RINGING_IN) &&
+                (call_info->state != MM_CALL_STATE_WAITING)) {
+                    mm_warn ("unexpected incoming call to number '%s' reported in call list: state %s",
+                             call_info->number ? call_info->number : "n/a",
+                             mm_call_state_get_string (call_info->state));
+                    continue;
+            }
+
+            mm_dbg ("Creating new incoming call...");
+            call = create_incoming_call (self, call_info->number);
+
+            /* Set the state and the index */
+            mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_INCOMING_NEW);
+            mm_base_call_set_index (call, call_info->index);
+
+            /* Start its validity timeout */
+            mm_base_call_incoming_refresh (call);
+
+            /* Only export once properly created */
+            mm_base_call_export (call);
+            mm_call_list_add_call (list, call);
+            g_object_unref (call);
+            continue;
+        }
+
+        mm_warn ("unexpected call to number '%s' reported in call list: state %s, direction unknown",
+                 call_info->number ? call_info->number : "n/a",
+                 mm_call_state_get_string (call_info->state));
+    }
+    g_list_free (ctx.call_info_list);
+    g_object_unref (list);
+}
+
+/*****************************************************************************/
+/* Incoming DTMF reception, not associated to a specific call */
+
+typedef struct {
+    guint        index;
+    const gchar *dtmf;
+} ReceivedDtmfContext;
+
+static void
+received_dtmf_foreach (MMBaseCall          *call,
+                       ReceivedDtmfContext *ctx)
+{
+    if ((!ctx->index || (ctx->index == mm_base_call_get_index (call))) &&
+        (mm_base_call_get_state (call) == MM_CALL_STATE_ACTIVE))
+        mm_base_call_received_dtmf (call, ctx->dtmf);
+}
+
+void
+mm_iface_modem_voice_received_dtmf (MMIfaceModemVoice *self,
+                                    guint              index,
+                                    const gchar       *dtmf)
+{
+    MMCallList          *list = NULL;
+    ReceivedDtmfContext  ctx = {
+        .index = index,
+        .dtmf  = dtmf
+    };
+
+    /* Retrieve list of known calls */
+    g_object_get (MM_BASE_MODEM (self),
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        mm_warn ("Cannot report received DTMF: missing call list");
+        return;
+    }
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc)received_dtmf_foreach, &ctx);
     g_object_unref (list);
 }
 
@@ -368,9 +694,1766 @@
 
 /*****************************************************************************/
 
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    GList                 *active_calls;
+    MMBaseCall            *next_call;
+} HandleHoldAndAcceptContext;
+
 static void
-update_message_list (MmGdbusModemVoice *skeleton,
-                     MMCallList *list)
+handle_hold_and_accept_context_free (HandleHoldAndAcceptContext *ctx)
+{
+    g_list_free_full (ctx->active_calls, g_object_unref);
+    g_clear_object (&ctx->next_call);
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleHoldAndAcceptContext, ctx);
+}
+
+static void
+hold_and_accept_ready (MMIfaceModemVoice          *self,
+                       GAsyncResult               *res,
+                       HandleHoldAndAcceptContext *ctx)
+{
+    GError *error = NULL;
+    GList  *l;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hold_and_accept_context_free (ctx);
+        return;
+    }
+
+    for (l = ctx->active_calls; l; l = g_list_next (l))
+        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
+    if (ctx->next_call)
+        mm_base_call_change_state (ctx->next_call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
+
+    mm_gdbus_modem_voice_complete_hold_and_accept (ctx->skeleton, ctx->invocation);
+    handle_hold_and_accept_context_free (ctx);
+}
+
+static void
+prepare_hold_and_accept_foreach (MMBaseCall                 *call,
+                                 HandleHoldAndAcceptContext *ctx)
+{
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_ACTIVE:
+        ctx->active_calls = g_list_append (ctx->active_calls, g_object_ref (call));
+        break;
+    case MM_CALL_STATE_WAITING:
+        g_clear_object (&ctx->next_call);
+        ctx->next_call = g_object_ref (call);
+        break;
+    case MM_CALL_STATE_HELD:
+        if (!ctx->next_call)
+            ctx->next_call = g_object_ref (call);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+handle_hold_and_accept_auth_ready (MMBaseModem                *self,
+                                   GAsyncResult               *res,
+                                   HandleHoldAndAcceptContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+    MMCallList   *list = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hold_and_accept_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hold and accept: device not yet enabled");
+        handle_hold_and_accept_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot hold and accept: unsupported");
+        handle_hold_and_accept_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hold and accept: missing call list");
+        handle_hold_and_accept_context_free (ctx);
+        return;
+    }
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hold_and_accept_foreach, ctx);
+    g_object_unref (list);
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept (MM_IFACE_MODEM_VOICE (self),
+                                                                (GAsyncReadyCallback)hold_and_accept_ready,
+                                                                ctx);
+}
+
+static gboolean
+handle_hold_and_accept (MmGdbusModemVoice     *skeleton,
+                        GDBusMethodInvocation *invocation,
+                        MMIfaceModemVoice     *self)
+{
+    HandleHoldAndAcceptContext *ctx;
+
+    ctx = g_slice_new0 (HandleHoldAndAcceptContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_hold_and_accept_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    GList                 *active_calls;
+    MMBaseCall            *next_call;
+} HandleHangupAndAcceptContext;
+
+static void
+handle_hangup_and_accept_context_free (HandleHangupAndAcceptContext *ctx)
+{
+    g_list_free_full (ctx->active_calls, g_object_unref);
+    g_clear_object (&ctx->next_call);
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleHangupAndAcceptContext, ctx);
+}
+
+static void
+hangup_and_accept_ready (MMIfaceModemVoice            *self,
+                         GAsyncResult                 *res,
+                         HandleHangupAndAcceptContext *ctx)
+{
+    GError *error = NULL;
+    GList  *l;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hangup_and_accept_context_free (ctx);
+        return;
+    }
+
+    for (l = ctx->active_calls; l; l = g_list_next (l))
+        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);
+    if (ctx->next_call)
+        mm_base_call_change_state (ctx->next_call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);
+
+    mm_gdbus_modem_voice_complete_hangup_and_accept (ctx->skeleton, ctx->invocation);
+    handle_hangup_and_accept_context_free (ctx);
+}
+
+static void
+prepare_hangup_and_accept_foreach (MMBaseCall                   *call,
+                                   HandleHangupAndAcceptContext *ctx)
+{
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_ACTIVE:
+        ctx->active_calls = g_list_append (ctx->active_calls, g_object_ref (call));
+        break;
+    case MM_CALL_STATE_WAITING:
+        g_clear_object (&ctx->next_call);
+        ctx->next_call = g_object_ref (call);
+        break;
+    case MM_CALL_STATE_HELD:
+        if (!ctx->next_call)
+            ctx->next_call = g_object_ref (call);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+handle_hangup_and_accept_auth_ready (MMBaseModem                  *self,
+                                     GAsyncResult                 *res,
+                                     HandleHangupAndAcceptContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+    MMCallList   *list = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hangup_and_accept_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hangup and accept: device not yet enabled");
+        handle_hangup_and_accept_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot hangup and accept: unsupported");
+        handle_hangup_and_accept_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hangup and accept: missing call list");
+        handle_hangup_and_accept_context_free (ctx);
+        return;
+    }
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hangup_and_accept_foreach, ctx);
+    g_object_unref (list);
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept (MM_IFACE_MODEM_VOICE (self),
+                                                                  (GAsyncReadyCallback)hangup_and_accept_ready,
+                                                                  ctx);
+}
+
+static gboolean
+handle_hangup_and_accept (MmGdbusModemVoice     *skeleton,
+                          GDBusMethodInvocation *invocation,
+                          MMIfaceModemVoice     *self)
+{
+    HandleHangupAndAcceptContext *ctx;
+
+    ctx = g_slice_new0 (HandleHangupAndAcceptContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_hangup_and_accept_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    GList                 *calls;
+} HandleHangupAllContext;
+
+static void
+handle_hangup_all_context_free (HandleHangupAllContext *ctx)
+{
+    g_list_free_full (ctx->calls, g_object_unref);
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleHangupAllContext, ctx);
+}
+
+static void
+hangup_all_ready (MMIfaceModemVoice      *self,
+                  GAsyncResult           *res,
+                  HandleHangupAllContext *ctx)
+{
+    GError *error = NULL;
+    GList  *l;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hangup_all_context_free (ctx);
+        return;
+    }
+
+    for (l = ctx->calls; l; l = g_list_next (l))
+        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);
+
+    mm_gdbus_modem_voice_complete_hangup_all (ctx->skeleton, ctx->invocation);
+    handle_hangup_all_context_free (ctx);
+}
+
+static void
+prepare_hangup_all_foreach (MMBaseCall             *call,
+                            HandleHangupAllContext *ctx)
+{
+    /* The implementation of this operation will usually be done with +CHUP, and we
+     * know that +CHUP is implemented in different ways by different manufacturers.
+     *
+     * The 3GPP TS27.007 spec for +CHUP states that the "Execution command causes
+     * the TA to hangup the current call of the MT." This sentence leaves a bit of open
+     * interpretation to the implementors, because a current call can be considered only
+     * the active ones, or otherwise any call (active, held or waiting).
+     *
+     * And so, the u-blox TOBY-L4 takes one interpretation and "In case of multiple
+     * calls, all active calls will be released, while waiting and held calls are not".
+     *
+     * And the Cinterion PLS-8 takes a different interpretation and cancels all calls,
+     * including the waiting and held ones.
+     *
+     * In this logic, we're going to terminate exclusively the ACTIVE calls only, and we
+     * will leave the possible termination of waiting/held calls to be reported via
+     * call state updates, e.g. +CLCC polling or other plugin-specific method. In the
+     * case of the Cinterion PLS-8, we'll detect the termination of the waiting and
+     * held calls via ^SLCC URCs.
+     */
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_DIALING:
+    case MM_CALL_STATE_RINGING_OUT:
+    case MM_CALL_STATE_RINGING_IN:
+    case MM_CALL_STATE_ACTIVE:
+        ctx->calls = g_list_append (ctx->calls, g_object_ref (call));
+        break;
+    case MM_CALL_STATE_WAITING:
+    case MM_CALL_STATE_HELD:
+    default:
+        break;
+    }
+}
+
+static void
+handle_hangup_all_auth_ready (MMBaseModem            *self,
+                              GAsyncResult           *res,
+                              HandleHangupAllContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+    MMCallList   *list = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_hangup_all_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hangup all: device not yet enabled");
+        handle_hangup_all_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot hangup all: unsupported");
+        handle_hangup_all_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot hangup all: missing call list");
+        handle_hangup_all_context_free (ctx);
+        return;
+    }
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hangup_all_foreach, ctx);
+    g_object_unref (list);
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all (MM_IFACE_MODEM_VOICE (self),
+                                                           (GAsyncReadyCallback)hangup_all_ready,
+                                                           ctx);
+}
+
+static gboolean
+handle_hangup_all (MmGdbusModemVoice     *skeleton,
+                   GDBusMethodInvocation *invocation,
+                   MMIfaceModemVoice     *self)
+{
+    HandleHangupAllContext *ctx;
+
+    ctx = g_slice_new0 (HandleHangupAllContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_hangup_all_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    GList                 *calls;
+} HandleTransferContext;
+
+static void
+handle_transfer_context_free (HandleTransferContext *ctx)
+{
+    g_list_free_full (ctx->calls, g_object_unref);
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleTransferContext, ctx);
+}
+
+static void
+transfer_ready (MMIfaceModemVoice     *self,
+                GAsyncResult          *res,
+                HandleTransferContext *ctx)
+{
+    GError *error = NULL;
+    GList  *l;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_transfer_context_free (ctx);
+        return;
+    }
+
+    for (l = ctx->calls; l; l = g_list_next (l))
+        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TRANSFERRED);
+
+    mm_gdbus_modem_voice_complete_transfer (ctx->skeleton, ctx->invocation);
+    handle_transfer_context_free (ctx);
+}
+
+static void
+prepare_transfer_foreach (MMBaseCall            *call,
+                          HandleTransferContext *ctx)
+{
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_ACTIVE:
+    case MM_CALL_STATE_HELD:
+        ctx->calls = g_list_append (ctx->calls, g_object_ref (call));
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+handle_transfer_auth_ready (MMBaseModem           *self,
+                            GAsyncResult          *res,
+                            HandleTransferContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+    MMCallList   *list = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_transfer_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot transfer: device not yet enabled");
+        handle_transfer_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot transfer: unsupported");
+        handle_transfer_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot transfer: missing call list");
+        handle_transfer_context_free (ctx);
+        return;
+    }
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_transfer_foreach, ctx);
+    g_object_unref (list);
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer (MM_IFACE_MODEM_VOICE (self),
+                                                         (GAsyncReadyCallback)transfer_ready,
+                                                         ctx);
+}
+
+static gboolean
+handle_transfer (MmGdbusModemVoice     *skeleton,
+                 GDBusMethodInvocation *invocation,
+                 MMIfaceModemVoice     *self)
+{
+    HandleTransferContext *ctx;
+
+    ctx = g_slice_new0 (HandleTransferContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_transfer_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    gboolean               enable;
+} HandleCallWaitingSetupContext;
+
+static void
+handle_call_waiting_setup_context_free (HandleCallWaitingSetupContext *ctx)
+{
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleCallWaitingSetupContext, ctx);
+}
+
+static void
+call_waiting_setup_ready (MMIfaceModemVoice             *self,
+                          GAsyncResult                  *res,
+                          HandleCallWaitingSetupContext *ctx)
+{
+    GError *error = NULL;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_call_waiting_setup_context_free (ctx);
+        return;
+    }
+
+    mm_gdbus_modem_voice_complete_call_waiting_setup (ctx->skeleton, ctx->invocation);
+    handle_call_waiting_setup_context_free (ctx);
+}
+
+static void
+handle_call_waiting_setup_auth_ready (MMBaseModem                   *self,
+                                      GAsyncResult                  *res,
+                                      HandleCallWaitingSetupContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_call_waiting_setup_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot setup call waiting: device not yet enabled");
+        handle_call_waiting_setup_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot setup call waiting: unsupported");
+        handle_call_waiting_setup_context_free (ctx);
+        return;
+    }
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup (MM_IFACE_MODEM_VOICE (self),
+                                                                   ctx->enable,
+                                                                   (GAsyncReadyCallback)call_waiting_setup_ready,
+                                                                   ctx);
+}
+
+static gboolean
+handle_call_waiting_setup (MmGdbusModemVoice     *skeleton,
+                           GDBusMethodInvocation *invocation,
+                           gboolean               enable,
+                           MMIfaceModemVoice     *self)
+{
+    HandleCallWaitingSetupContext *ctx;
+
+    ctx = g_slice_new0 (HandleCallWaitingSetupContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+    ctx->enable     = enable;
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_call_waiting_setup_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    MmGdbusModemVoice     *skeleton;
+    GDBusMethodInvocation *invocation;
+    MMIfaceModemVoice     *self;
+    gboolean               enable;
+} HandleCallWaitingQueryContext;
+
+static void
+handle_call_waiting_query_context_free (HandleCallWaitingQueryContext *ctx)
+{
+    g_object_unref (ctx->skeleton);
+    g_object_unref (ctx->invocation);
+    g_object_unref (ctx->self);
+    g_slice_free (HandleCallWaitingQueryContext, ctx);
+}
+
+static void
+call_waiting_query_ready (MMIfaceModemVoice             *self,
+                          GAsyncResult                  *res,
+                          HandleCallWaitingQueryContext *ctx)
+{
+    GError   *error = NULL;
+    gboolean  status = FALSE;
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query_finish (self, res, &status, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_call_waiting_query_context_free (ctx);
+        return;
+    }
+
+    mm_gdbus_modem_voice_complete_call_waiting_query (ctx->skeleton, ctx->invocation, status);
+    handle_call_waiting_query_context_free (ctx);
+}
+
+static void
+handle_call_waiting_query_auth_ready (MMBaseModem                   *self,
+                                      GAsyncResult                  *res,
+                                      HandleCallWaitingQueryContext *ctx)
+{
+    MMModemState  modem_state = MM_MODEM_STATE_UNKNOWN;
+    GError       *error = NULL;
+
+    if (!mm_base_modem_authorize_finish (self, res, &error)) {
+        g_dbus_method_invocation_take_error (ctx->invocation, error);
+        handle_call_waiting_query_context_free (ctx);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_STATE, &modem_state,
+                  NULL);
+
+    if (modem_state < MM_MODEM_STATE_ENABLED) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_WRONG_STATE,
+                                               "Cannot query call waiting: device not yet enabled");
+        handle_call_waiting_query_context_free (ctx);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query_finish) {
+        g_dbus_method_invocation_return_error (ctx->invocation,
+                                               MM_CORE_ERROR,
+                                               MM_CORE_ERROR_UNSUPPORTED,
+                                               "Cannot query call waiting: unsupported");
+        handle_call_waiting_query_context_free (ctx);
+        return;
+    }
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query (MM_IFACE_MODEM_VOICE (self),
+                                                                   (GAsyncReadyCallback)call_waiting_query_ready,
+                                                                   ctx);
+}
+
+static gboolean
+handle_call_waiting_query (MmGdbusModemVoice     *skeleton,
+                           GDBusMethodInvocation *invocation,
+                           MMIfaceModemVoice     *self)
+{
+    HandleCallWaitingQueryContext *ctx;
+
+    ctx = g_slice_new0 (HandleCallWaitingQueryContext);
+    ctx->skeleton   = g_object_ref (skeleton);
+    ctx->invocation = g_object_ref (invocation);
+    ctx->self       = g_object_ref (self);
+
+    mm_base_modem_authorize (MM_BASE_MODEM (self),
+                             invocation,
+                             MM_AUTHORIZATION_VOICE,
+                             (GAsyncReadyCallback)handle_call_waiting_query_auth_ready,
+                             ctx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Leave one of the calls from the multiparty call */
+
+typedef struct {
+    MMBaseCall *call;
+    GList      *other_calls;
+} LeaveMultipartyContext;
+
+static void
+leave_multiparty_context_free (LeaveMultipartyContext *ctx)
+{
+    g_list_free_full (ctx->other_calls, g_object_unref);
+    g_object_unref (ctx->call);
+    g_slice_free (LeaveMultipartyContext, ctx);
+}
+
+static void
+prepare_leave_multiparty_foreach (MMBaseCall             *call,
+                                  LeaveMultipartyContext *ctx)
+{
+    /* ignore call that is leaving */
+    if ((call == ctx->call) || (g_strcmp0 (mm_base_call_get_path (call), mm_base_call_get_path (ctx->call)) == 0))
+        return;
+
+    /* ignore non-multiparty calls */
+    if (!mm_base_call_get_multiparty (call))
+        return;
+
+    /* ignore calls not currently ongoing */
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_ACTIVE:
+    case MM_CALL_STATE_HELD:
+        ctx->other_calls = g_list_append (ctx->other_calls, g_object_ref (call));
+        break;
+    default:
+        break;
+    }
+}
+
+gboolean
+mm_iface_modem_voice_leave_multiparty_finish (MMIfaceModemVoice  *self,
+                                              GAsyncResult       *res,
+                                              GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+leave_multiparty_ready (MMIfaceModemVoice *self,
+                        GAsyncResult      *res,
+                        GTask             *task)
+{
+    GError                 *error = NULL;
+    LeaveMultipartyContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* If there is only one remaining call that was part of the multiparty, consider that
+     * one also no longer part of any multiparty, and put it on hold right away */
+    if (g_list_length (ctx->other_calls) == 1) {
+        mm_base_call_set_multiparty (MM_BASE_CALL (ctx->other_calls->data), FALSE);
+        mm_base_call_change_state (MM_BASE_CALL (ctx->other_calls->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
+    }
+    /* If there are still more than one calls in the multiparty, just change state of all
+     * of them. */
+    else {
+        GList *l;
+
+        for (l = ctx->other_calls; l; l = g_list_next (l))
+            mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
+    }
+
+    /* The call that left would now be active */
+    mm_base_call_set_multiparty (ctx->call, FALSE);
+    mm_base_call_change_state (ctx->call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_UNKNOWN);
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_iface_modem_voice_leave_multiparty (MMIfaceModemVoice   *self,
+                                       MMBaseCall          *call,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+    GTask                  *task;
+    LeaveMultipartyContext *ctx;
+    MMCallList             *list = NULL;
+    MMCallState             call_state;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* validate multiparty status */
+    if (!mm_base_call_get_multiparty (call)) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "this call is not part of a multiparty call");
+        g_object_unref (task);
+        return;
+    }
+    /* validate call state */
+    call_state = mm_base_call_get_state (call);
+    if ((call_state != MM_CALL_STATE_ACTIVE) && (call_state != MM_CALL_STATE_HELD)) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "invalid call state (%s): must be either active or held",
+                                 mm_call_state_get_string (call_state));
+        g_object_unref (task);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty_finish) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
+                                 "Cannot leave multiparty: unsupported");
+        g_object_unref (task);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "Cannot leave multiparty: missing call list");
+        g_object_unref (task);
+        return;
+    }
+
+    ctx = g_slice_new0 (LeaveMultipartyContext);
+    ctx->call = g_object_ref (call);
+    g_task_set_task_data (task, ctx, (GDestroyNotify) leave_multiparty_context_free);
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_leave_multiparty_foreach, ctx);
+    g_object_unref (list);
+
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty (self,
+                                                                 call,
+                                                                 (GAsyncReadyCallback)leave_multiparty_ready,
+                                                                 task);
+}
+
+/*****************************************************************************/
+/* Join calls into a multiparty call */
+
+typedef struct {
+    MMBaseCall *call;
+    GList      *all_calls;
+    gboolean    added;
+} JoinMultipartyContext;
+
+static void
+join_multiparty_context_free (JoinMultipartyContext *ctx)
+{
+    g_list_free_full (ctx->all_calls, g_object_unref);
+    g_object_unref (ctx->call);
+    g_slice_free (JoinMultipartyContext, ctx);
+}
+
+static void
+prepare_join_multiparty_foreach (MMBaseCall            *call,
+                                 JoinMultipartyContext *ctx)
+{
+    /* always add call that is being added */
+    if ((call == ctx->call) || (g_strcmp0 (mm_base_call_get_path (call), mm_base_call_get_path (ctx->call)) == 0))
+        ctx->added = TRUE;
+
+    /* ignore calls not currently ongoing */
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_ACTIVE:
+    case MM_CALL_STATE_HELD:
+        ctx->all_calls = g_list_append (ctx->all_calls, g_object_ref (call));
+        break;
+    default:
+        break;
+    }
+}
+
+gboolean
+mm_iface_modem_voice_join_multiparty_finish (MMIfaceModemVoice  *self,
+                                             GAsyncResult       *res,
+                                             GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+join_multiparty_ready (MMIfaceModemVoice *self,
+                       GAsyncResult      *res,
+                       GTask             *task)
+{
+    GError                *error = NULL;
+    JoinMultipartyContext *ctx;
+    GList                 *l;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty_finish (self, res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    for (l = ctx->all_calls; l; l = g_list_next (l)) {
+        mm_base_call_set_multiparty (MM_BASE_CALL (l->data), TRUE);
+        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_UNKNOWN);
+    }
+
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_iface_modem_voice_join_multiparty (MMIfaceModemVoice   *self,
+                                      MMBaseCall          *call,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+    GTask                 *task;
+    JoinMultipartyContext *ctx;
+    MMCallList            *list = NULL;
+    MMCallState            call_state;
+
+    task = g_task_new (self, NULL, callback, user_data);
+
+    /* validate multiparty status */
+    if (mm_base_call_get_multiparty (call)) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "this call is already part of a multiparty call");
+        g_object_unref (task);
+        return;
+    }
+    /* validate call state */
+    call_state = mm_base_call_get_state (call);
+    if (call_state != MM_CALL_STATE_HELD) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "invalid call state (%s): must be held",
+                                 mm_call_state_get_string (call_state));
+        g_object_unref (task);
+        return;
+    }
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty ||
+        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty_finish) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
+                                 "Cannot join multiparty: unsupported");
+        g_object_unref (task);
+        return;
+    }
+
+    g_object_get (self,
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
+                                 "Cannot join multiparty: missing call list");
+        g_object_unref (task);
+        return;
+    }
+
+    ctx = g_slice_new0 (JoinMultipartyContext);
+    ctx->call = g_object_ref (call);
+    g_task_set_task_data (task, ctx, (GDestroyNotify) join_multiparty_context_free);
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_join_multiparty_foreach, ctx);
+    g_object_unref (list);
+
+    /* our logic makes sure that we would be adding the incoming call into the multiparty call */
+    g_assert (ctx->added);
+
+    /* NOTE: we do not give the call we want to join, because the join operation acts on all
+     * active/held calls. */
+    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty (self,
+                                                                (GAsyncReadyCallback)join_multiparty_ready,
+                                                                task);
+}
+
+/*****************************************************************************/
+/* In-call setup operation
+ *
+ * It will setup URC handlers for all in-call URCs, and also setup the audio
+ * channel if the plugin requires to do so.
+ */
+
+typedef enum {
+    IN_CALL_SETUP_STEP_FIRST,
+    IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS,
+    IN_CALL_SETUP_STEP_AUDIO_CHANNEL,
+    IN_CALL_SETUP_STEP_LAST,
+} InCallSetupStep;
+
+typedef struct {
+    InCallSetupStep    step;
+    MMPort            *audio_port;
+    MMCallAudioFormat *audio_format;
+} InCallSetupContext;
+
+static void
+in_call_setup_context_free (InCallSetupContext *ctx)
+{
+    g_clear_object (&ctx->audio_port);
+    g_clear_object (&ctx->audio_format);
+    g_slice_free (InCallSetupContext, ctx);
+}
+
+static gboolean
+in_call_setup_finish (MMIfaceModemVoice  *self,
+                      GAsyncResult       *res,
+                      MMPort            **audio_port,   /* optional */
+                      MMCallAudioFormat **audio_format, /* optional */
+                      GError            **error)
+{
+    InCallSetupContext *ctx;
+
+    if (!g_task_propagate_boolean (G_TASK (res), error))
+        return FALSE;
+
+    ctx = g_task_get_task_data (G_TASK (res));
+    if (audio_port) {
+        *audio_port = ctx->audio_port;
+        ctx->audio_port = NULL;
+    }
+    if (audio_format) {
+        *audio_format = ctx->audio_format;
+        ctx->audio_format = NULL;
+    }
+
+    return TRUE;
+}
+
+static void in_call_setup_context_step (GTask *task);
+
+static void
+setup_in_call_audio_channel_ready (MMIfaceModemVoice *self,
+                                   GAsyncResult      *res,
+                                   GTask             *task)
+{
+    InCallSetupContext *ctx;
+    GError             *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish (self,
+                                                                                        res,
+                                                                                        &ctx->audio_port,
+                                                                                        &ctx->audio_format,
+                                                                                        &error)) {
+        mm_warn ("Couldn't setup in-call audio channel: %s", error->message);
+        g_clear_error (&error);
+    }
+
+    ctx->step++;
+    in_call_setup_context_step (task);
+}
+
+static void
+setup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                        GAsyncResult      *res,
+                                        GTask             *task)
+{
+    InCallSetupContext *ctx;
+    GError             *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't setup in-call unsolicited events: %s", error->message);
+        g_clear_error (&error);
+    }
+
+    ctx->step++;
+    in_call_setup_context_step (task);
+}
+
+static void
+in_call_setup_context_step (GTask *task)
+{
+    MMIfaceModemVoice  *self;
+    InCallSetupContext *ctx;
+
+    if (g_task_return_error_if_cancelled (task))
+        return;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    switch (ctx->step) {
+    case IN_CALL_SETUP_STEP_FIRST:
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS:
+        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events &&
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish) {
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events (
+                self,
+                (GAsyncReadyCallback) setup_in_call_unsolicited_events_ready,
+                task);
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_SETUP_STEP_AUDIO_CHANNEL:
+        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel &&
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish) {
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel (
+                self,
+                (GAsyncReadyCallback) setup_in_call_audio_channel_ready,
+                task);
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_SETUP_STEP_LAST:
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+}
+
+static void
+in_call_setup (MMIfaceModemVoice   *self,
+               GCancellable        *cancellable,
+               GAsyncReadyCallback  callback,
+               gpointer             user_data)
+{
+    GTask              *task;
+    InCallSetupContext *ctx;
+
+    ctx = g_slice_new0 (InCallSetupContext);
+    ctx->step = IN_CALL_SETUP_STEP_FIRST;
+
+    task = g_task_new (self, cancellable, callback, user_data);
+    g_task_set_task_data (task, ctx, (GDestroyNotify) in_call_setup_context_free);
+
+    in_call_setup_context_step (task);
+}
+
+/*****************************************************************************/
+/* In-call cleanup operation
+ *
+ * It will cleanup audio channel settings and remove all in-call URC handlers.
+ */
+
+typedef enum {
+    IN_CALL_CLEANUP_STEP_FIRST,
+    IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL,
+    IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS,
+    IN_CALL_CLEANUP_STEP_LAST,
+} InCallCleanupStep;
+
+typedef struct {
+    InCallCleanupStep step;
+} InCallCleanupContext;
+
+static gboolean
+in_call_cleanup_finish (MMIfaceModemVoice  *self,
+                        GAsyncResult       *res,
+                        GError            **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void in_call_cleanup_context_step (GTask *task);
+
+static void
+cleanup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self,
+                                          GAsyncResult      *res,
+                                          GTask             *task)
+{
+    InCallCleanupContext *ctx;
+    GError             *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup in-call unsolicited events: %s", error->message);
+        g_clear_error (&error);
+    }
+
+    ctx->step++;
+    in_call_cleanup_context_step (task);
+}
+
+static void
+cleanup_in_call_audio_channel_ready (MMIfaceModemVoice *self,
+                                     GAsyncResult      *res,
+                                     GTask             *task)
+{
+    InCallCleanupContext *ctx;
+    GError             *error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish (self, res, &error)) {
+        mm_warn ("Couldn't cleanup in-call audio channel: %s", error->message);
+        g_clear_error (&error);
+    }
+
+    ctx->step++;
+    in_call_cleanup_context_step (task);
+}
+
+static void
+in_call_cleanup_context_step (GTask *task)
+{
+    MMIfaceModemVoice    *self;
+    InCallCleanupContext *ctx;
+
+    if (g_task_return_error_if_cancelled (task))
+        return;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    switch (ctx->step) {
+    case IN_CALL_CLEANUP_STEP_FIRST:
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL:
+        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel &&
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish) {
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel (
+                self,
+                (GAsyncReadyCallback) cleanup_in_call_audio_channel_ready,
+                task);
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS:
+        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events &&
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish) {
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events (
+                self,
+                (GAsyncReadyCallback) cleanup_in_call_unsolicited_events_ready,
+                task);
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case IN_CALL_CLEANUP_STEP_LAST:
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+}
+
+static void
+in_call_cleanup (MMIfaceModemVoice   *self,
+                 GCancellable        *cancellable,
+                 GAsyncReadyCallback  callback,
+                 gpointer             user_data)
+{
+    GTask                *task;
+    InCallCleanupContext *ctx;
+
+    ctx = g_new0 (InCallCleanupContext, 1);
+    ctx->step = IN_CALL_CLEANUP_STEP_FIRST;
+
+    task = g_task_new (self, cancellable, callback, user_data);
+    g_task_set_task_data (task, ctx, g_free);
+
+    in_call_cleanup_context_step (task);
+}
+
+/*****************************************************************************/
+/* In-call event handling logic
+ *
+ * This procedure will run a in-call setup async function whenever we detect
+ * that there is at least one call that is ongoing. This setup function will
+ * try to setup in-call unsolicited events as well as any audio channel
+ * requirements.
+ *
+ * The procedure will run a in-call cleanup async function whenever we detect
+ * that there are no longer any ongoing calls. The cleanup function will
+ * cleanup the audio channel and remove the in-call unsolicited event handlers.
+ */
+
+typedef struct {
+    guint              check_id;
+    GCancellable      *setup_cancellable;
+    GCancellable      *cleanup_cancellable;
+    gboolean           in_call_state;
+    MMPort            *audio_port;
+    MMCallAudioFormat *audio_format;
+} InCallEventContext;
+
+static void
+in_call_event_context_free (InCallEventContext *ctx)
+{
+    if (ctx->check_id)
+        g_source_remove (ctx->check_id);
+    if (ctx->cleanup_cancellable) {
+        g_cancellable_cancel (ctx->cleanup_cancellable);
+        g_clear_object (&ctx->cleanup_cancellable);
+    }
+    if (ctx->setup_cancellable) {
+        g_cancellable_cancel (ctx->setup_cancellable);
+        g_clear_object (&ctx->setup_cancellable);
+    }
+    g_clear_object (&ctx->audio_port);
+    g_clear_object (&ctx->audio_format);
+    g_slice_free (InCallEventContext, ctx);
+}
+
+static InCallEventContext *
+get_in_call_event_context (MMIfaceModemVoice *self)
+{
+    InCallEventContext *ctx;
+
+    if (G_UNLIKELY (!in_call_event_context_quark))
+        in_call_event_context_quark = g_quark_from_static_string (IN_CALL_EVENT_CONTEXT_TAG);
+
+    ctx = g_object_get_qdata (G_OBJECT (self), in_call_event_context_quark);
+    if (!ctx) {
+        /* Create context and keep it as object data */
+        ctx = g_slice_new0 (InCallEventContext);
+        g_object_set_qdata_full (
+            G_OBJECT (self),
+            in_call_event_context_quark,
+            ctx,
+            (GDestroyNotify)in_call_event_context_free);
+    }
+
+    return ctx;
+}
+
+static void
+call_list_foreach_audio_settings (MMBaseCall         *call,
+                                  InCallEventContext *ctx)
+{
+    if (mm_base_call_get_state (call) != MM_CALL_STATE_TERMINATED)
+        return;
+    mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format);
+}
+
+static void
+update_audio_settings_in_ongoing_calls (MMIfaceModemVoice *self)
+{
+    MMCallList         *list = NULL;
+    InCallEventContext *ctx;
+
+    ctx = get_in_call_event_context (self);
+
+    g_object_get (MM_BASE_MODEM (self),
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        mm_warn ("Cannot update audio settings in active calls: missing internal call list");
+        return;
+    }
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_audio_settings, ctx);
+    g_clear_object (&list);
+}
+
+static void
+update_audio_settings_in_call (MMIfaceModemVoice *self,
+                               MMBaseCall        *call)
+{
+    InCallEventContext *ctx;
+
+    ctx = get_in_call_event_context (self);
+    mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format);
+}
+
+static void
+call_list_foreach_count_in_call (MMBaseCall *call,
+                                 gpointer    user_data)
+{
+    guint *n_calls_in_call = (guint *)user_data;
+
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_DIALING:
+    case MM_CALL_STATE_RINGING_OUT:
+    case MM_CALL_STATE_HELD:
+    case MM_CALL_STATE_ACTIVE:
+        *n_calls_in_call = *n_calls_in_call + 1;
+        break;
+    case MM_CALL_STATE_RINGING_IN:
+    case MM_CALL_STATE_WAITING:
+        /* NOTE: ringing-in and waiting calls are NOT yet in-call, e.g. there must
+         * be no audio settings enabled and we must not enable in-call URC handling
+         * yet. */
+    default:
+        break;
+    }
+}
+
+static void
+in_call_cleanup_ready (MMIfaceModemVoice *self,
+                       GAsyncResult      *res)
+{
+    GError             *error = NULL;
+    InCallEventContext *ctx;
+
+    ctx = get_in_call_event_context (self);
+
+    if (!in_call_cleanup_finish (self, res, &error)) {
+        /* ignore cancelled operations */
+        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED))
+            mm_warn ("Cannot cleanup in-call modem state: %s", error->message);
+        g_clear_error (&error);
+    } else {
+        mm_dbg ("modem is no longer in-call state");
+        ctx->in_call_state = FALSE;
+        g_clear_object (&ctx->audio_port);
+        g_clear_object (&ctx->audio_format);
+    }
+
+    g_clear_object (&ctx->cleanup_cancellable);
+}
+
+static void
+in_call_setup_ready (MMIfaceModemVoice *self,
+                     GAsyncResult      *res)
+{
+    GError             *error = NULL;
+    InCallEventContext *ctx;
+
+    ctx = get_in_call_event_context (self);
+
+    if (!in_call_setup_finish (self, res, &ctx->audio_port, &ctx->audio_format, &error)) {
+        /* ignore cancelled operations */
+        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED))
+            mm_warn ("Cannot setup in-call modem state: %s", error->message);
+        g_clear_error (&error);
+    } else {
+        mm_dbg ("modem is now in-call state");
+        ctx->in_call_state = TRUE;
+        update_audio_settings_in_ongoing_calls (self);
+    }
+
+    g_clear_object (&ctx->setup_cancellable);
+}
+
+static gboolean
+call_list_check_in_call_events (MMIfaceModemVoice *self)
+{
+    InCallEventContext *ctx;
+    MMCallList         *list = NULL;
+    guint               n_calls_in_call = 0;
+
+    ctx = get_in_call_event_context (self);
+    ctx->check_id = 0;
+
+    g_object_get (MM_BASE_MODEM (self),
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+    if (!list) {
+        mm_warn ("Cannot update in-call state: missing internal call list");
+        goto out;
+    }
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_count_in_call, &n_calls_in_call);
+
+    /* Need to setup in-call events? */
+    if (n_calls_in_call > 0 && !ctx->in_call_state) {
+        /* if setup already ongoing, do nothing */
+        if (ctx->setup_cancellable)
+            goto out;
+
+        /* cancel ongoing cleanup if any */
+        if (ctx->cleanup_cancellable) {
+            g_cancellable_cancel (ctx->cleanup_cancellable);
+            g_clear_object (&ctx->cleanup_cancellable);
+        }
+
+        /* run setup */
+        mm_dbg ("Setting up in-call state...");
+        ctx->setup_cancellable = g_cancellable_new ();
+        in_call_setup (self, ctx->setup_cancellable, (GAsyncReadyCallback) in_call_setup_ready, NULL);
+        goto out;
+    }
+
+    /* Need to cleanup in-call events? */
+    if (n_calls_in_call == 0 && ctx->in_call_state) {
+        /* if cleanup already ongoing, do nothing */
+        if (ctx->cleanup_cancellable)
+            goto out;
+
+        /* cancel ongoing setup if any */
+        if (ctx->setup_cancellable) {
+            g_cancellable_cancel (ctx->setup_cancellable);
+            g_clear_object (&ctx->setup_cancellable);
+        }
+
+        /* run cleanup */
+        mm_dbg ("Cleaning up in-call state...");
+        ctx->cleanup_cancellable = g_cancellable_new ();
+        in_call_cleanup (self, ctx->cleanup_cancellable, (GAsyncReadyCallback) in_call_cleanup_ready, NULL);
+        goto out;
+    }
+
+ out:
+    g_clear_object (&list);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+call_state_changed (MMIfaceModemVoice *self)
+{
+    InCallEventContext *ctx;
+
+    ctx = get_in_call_event_context (self);
+    if (ctx->check_id)
+        return;
+
+    /* Process check for in-call events in an idle, so that we can combine
+     * together in the same check multiple call state updates happening
+     * at the same time for different calls (e.g. when swapping active/held
+     * calls). */
+    ctx->check_id = g_idle_add ((GSourceFunc)call_list_check_in_call_events, self);
+}
+
+static void
+setup_in_call_event_handling (MMCallList        *call_list,
+                              const gchar       *call_path_added,
+                              MMIfaceModemVoice *self)
+{
+    MMBaseCall *call;
+
+    call = mm_call_list_get_call (call_list, call_path_added);
+    g_assert (call);
+
+    g_signal_connect_swapped (call,
+                              "state-changed",
+                              G_CALLBACK (call_state_changed),
+                              self);
+}
+
+/*****************************************************************************/
+/* Call list polling logic
+ *
+ * The call list polling is exclusively used to detect detailed call state
+ * updates while a call is being established. Therefore, if there is no call
+ * being established (i.e. all terminated, unknown or active), then there is
+ * no polling to do.
+ *
+ * Any time we add a new call to the list, we'll setup polling if it's not
+ * already running, and the polling logic itself will decide when the polling
+ * should stop.
+ */
+
+#define CALL_LIST_POLLING_TIMEOUT_SECS 2
+
+typedef struct {
+    guint    polling_id;
+    gboolean polling_ongoing;
+} CallListPollingContext;
+
+static void
+call_list_polling_context_free (CallListPollingContext *ctx)
+{
+    if (ctx->polling_id)
+        g_source_remove (ctx->polling_id);
+    g_slice_free (CallListPollingContext, ctx);
+}
+
+static CallListPollingContext *
+get_call_list_polling_context (MMIfaceModemVoice *self)
+{
+    CallListPollingContext *ctx;
+
+    if (G_UNLIKELY (!call_list_polling_context_quark))
+        call_list_polling_context_quark =  (g_quark_from_static_string (
+                                                 CALL_LIST_POLLING_CONTEXT_TAG));
+
+    ctx = g_object_get_qdata (G_OBJECT (self), call_list_polling_context_quark);
+    if (!ctx) {
+        /* Create context and keep it as object data */
+        ctx = g_slice_new0 (CallListPollingContext);
+
+        g_object_set_qdata_full (
+            G_OBJECT (self),
+            call_list_polling_context_quark,
+            ctx,
+            (GDestroyNotify)call_list_polling_context_free);
+    }
+
+    return ctx;
+}
+
+static gboolean call_list_poll (MMIfaceModemVoice *self);
+
+static void
+load_call_list_ready (MMIfaceModemVoice *self,
+                      GAsyncResult      *res)
+{
+    CallListPollingContext *ctx;
+    GList                  *call_info_list = NULL;
+    GError                 *error = NULL;
+
+    ctx = get_call_list_polling_context (self);
+    ctx->polling_ongoing = FALSE;
+
+    g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish);
+    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish (self, res, &call_info_list, &error)) {
+        mm_warn ("couldn't load call list: %s", error->message);
+        g_error_free (error);
+    }
+
+    /* Always report the list even if NULL (it would mean no ongoing calls) */
+    mm_iface_modem_voice_report_all_calls (self, call_info_list);
+    mm_3gpp_call_info_list_free (call_info_list);
+
+    /* setup the polling again */
+    g_assert (!ctx->polling_id);
+    ctx->polling_id = g_timeout_add_seconds (CALL_LIST_POLLING_TIMEOUT_SECS,
+                                             (GSourceFunc) call_list_poll,
+                                             self);
+}
+
+static void
+call_list_foreach_count_establishing (MMBaseCall *call,
+                                      gpointer    user_data)
+{
+    guint *n_calls_establishing = (guint *)user_data;
+
+    switch (mm_base_call_get_state (call)) {
+    case MM_CALL_STATE_DIALING:
+    case MM_CALL_STATE_RINGING_OUT:
+    case MM_CALL_STATE_RINGING_IN:
+    case MM_CALL_STATE_HELD:
+    case MM_CALL_STATE_WAITING:
+        *n_calls_establishing = *n_calls_establishing + 1;
+        break;
+    default:
+        break;
+    }
+}
+
+static gboolean
+call_list_poll (MMIfaceModemVoice *self)
+{
+    CallListPollingContext *ctx;
+    MMCallList             *list = NULL;
+    guint                   n_calls_establishing = 0;
+
+    ctx = get_call_list_polling_context (self);
+    ctx->polling_id = 0;
+
+    g_object_get (MM_BASE_MODEM (self),
+                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
+                  NULL);
+
+    if (!list) {
+        mm_warn ("Cannot poll call list: missing internal call list");
+        goto out;
+    }
+
+    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_count_establishing, &n_calls_establishing);
+
+    /* If there is at least ONE call being established, we need the call list */
+    if (n_calls_establishing > 0) {
+        mm_dbg ("%u calls being established: call list polling required", n_calls_establishing);
+        ctx->polling_ongoing = TRUE;
+        g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list);
+        MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list (self,
+                                                                   (GAsyncReadyCallback)load_call_list_ready,
+                                                                   NULL);
+    } else
+        mm_dbg ("no calls being established: call list polling stopped");
+
+out:
+    g_clear_object (&list);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+setup_call_list_polling (MMCallList        *call_list,
+                         const gchar       *call_path_added,
+                         MMIfaceModemVoice *self)
+{
+    CallListPollingContext *ctx;
+
+    ctx = get_call_list_polling_context (self);
+
+    if (!ctx->polling_id && !ctx->polling_ongoing)
+        ctx->polling_id = g_timeout_add_seconds (CALL_LIST_POLLING_TIMEOUT_SECS,
+                                                 (GSourceFunc) call_list_poll,
+                                                 self);
+}
+
+/*****************************************************************************/
+
+static void
+update_call_list (MmGdbusModemVoice *skeleton,
+                  MMCallList        *list)
 {
     gchar **paths;
 
@@ -382,22 +2465,22 @@
 }
 
 static void
-call_added (MMCallList *list,
-           const gchar *call_path,
-           MmGdbusModemVoice *skeleton)
+call_added (MMCallList        *list,
+            const gchar       *call_path,
+            MmGdbusModemVoice *skeleton)
 {
-    mm_dbg ("Added CALL at '%s'", call_path);
-    update_message_list (skeleton, list);
+    mm_dbg ("Added call at '%s'", call_path);
+    update_call_list (skeleton, list);
     mm_gdbus_modem_voice_emit_call_added (skeleton, call_path);
 }
 
 static void
-call_deleted (MMCallList *list,
-             const gchar *call_path,
-             MmGdbusModemVoice *skeleton)
+call_deleted (MMCallList        *list,
+              const gchar       *call_path,
+              MmGdbusModemVoice *skeleton)
 {
-    mm_dbg ("Deleted CALL at '%s'", call_path);
-    update_message_list (skeleton, list);
+    mm_dbg ("Deleted call at '%s'", call_path);
+    update_call_list (skeleton, list);
     mm_gdbus_modem_voice_emit_call_deleted (skeleton, call_path);
 }
 
@@ -669,6 +2752,29 @@
                           G_CALLBACK (call_deleted),
                           ctx->skeleton);
 
+        /* Setup monitoring for in-call event handling */
+        g_signal_connect (list,
+                          MM_CALL_ADDED,
+                          G_CALLBACK (setup_in_call_event_handling),
+                          self);
+
+        /* Unless we're told not to, setup call list polling logic */
+        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list &&
+            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish) {
+            gboolean periodic_call_list_check_disabled = FALSE;
+
+            g_object_get (self,
+                          MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED, &periodic_call_list_check_disabled,
+                          NULL);
+            if (!periodic_call_list_check_disabled) {
+                mm_dbg ("periodic call list polling will be used if supported");
+                g_signal_connect (list,
+                                  MM_CALL_ADDED,
+                                  G_CALLBACK (setup_call_list_polling),
+                                  self);
+            }
+        }
+
         g_object_unref (list);
 
         /* Fall down to next step */
@@ -863,21 +2969,18 @@
         ctx->step++;
 
     case INITIALIZATION_STEP_LAST:
-        /* We are done without errors! */
-
-        /* Handle method invocations */
-        g_signal_connect (ctx->skeleton,
-                          "handle-create-call",
-                          G_CALLBACK (handle_create),
-                          self);
-        g_signal_connect (ctx->skeleton,
-                          "handle-delete-call",
-                          G_CALLBACK (handle_delete),
-                          self);
-        g_signal_connect (ctx->skeleton,
-                          "handle-list-calls",
-                          G_CALLBACK (handle_list),
-                          self);
+        /* Setup all method handlers */
+        g_object_connect (ctx->skeleton,
+                          "signal::handle-create-call",        G_CALLBACK (handle_create),             self,
+                          "signal::handle-delete-call",        G_CALLBACK (handle_delete),             self,
+                          "signal::handle-list-calls",         G_CALLBACK (handle_list),               self,
+                          "signal::handle-hangup-and-accept",  G_CALLBACK (handle_hangup_and_accept),  self,
+                          "signal::handle-hold-and-accept",    G_CALLBACK (handle_hold_and_accept),    self,
+                          "signal::handle-hangup-all",         G_CALLBACK (handle_hangup_all),         self,
+                          "signal::handle-transfer",           G_CALLBACK (handle_transfer),           self,
+                          "signal::handle-call-waiting-setup", G_CALLBACK (handle_call_waiting_setup), self,
+                          "signal::handle-call-waiting-query", G_CALLBACK (handle_call_waiting_query), self,
+                          NULL);
 
         /* Finally, export the new interface */
         mm_gdbus_object_skeleton_set_modem_voice (MM_GDBUS_OBJECT_SKELETON (self),
@@ -970,6 +3073,14 @@
                               MM_TYPE_CALL_LIST,
                               G_PARAM_READWRITE));
 
+    g_object_interface_install_property
+        (g_iface,
+         g_param_spec_boolean (MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED,
+                               "Periodic call list checks disabled",
+                               "Whether periodic call list check are disabled.",
+                               FALSE,
+                               G_PARAM_READWRITE));
+
     initialized = TRUE;
 }
 
diff --git a/src/mm-iface-modem-voice.h b/src/mm-iface-modem-voice.h
index e82c546..7f793dc 100644
--- a/src/mm-iface-modem-voice.h
+++ b/src/mm-iface-modem-voice.h
@@ -10,7 +10,8 @@
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details:
  *
- * Copyright (C) 2015 - Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2015 Marco Bascetta <marco.bascetta@sadel.it>
+ * Copyright (C) 2019 Purism SPC
  */
 
 #ifndef MM_IFACE_MODEM_VOICE_H
@@ -29,8 +30,9 @@
 #define MM_IS_IFACE_MODEM_VOICE(obj)            (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_IFACE_MODEM_VOICE))
 #define MM_IFACE_MODEM_VOICE_GET_INTERFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), MM_TYPE_IFACE_MODEM_VOICE, MMIfaceModemVoice))
 
-#define MM_IFACE_MODEM_VOICE_DBUS_SKELETON          "iface-modem-voice-dbus-skeleton"
-#define MM_IFACE_MODEM_VOICE_CALL_LIST              "iface-modem-voice-call-list"
+#define MM_IFACE_MODEM_VOICE_DBUS_SKELETON                     "iface-modem-voice-dbus-skeleton"
+#define MM_IFACE_MODEM_VOICE_CALL_LIST                         "iface-modem-voice-call-list"
+#define MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED "iface-modem-voice-periodic-call-list-check-disabled"
 
 typedef struct _MMIfaceModemVoice MMIfaceModemVoice;
 
@@ -77,10 +79,120 @@
                                                     GAsyncResult *res,
                                                     GError **error);
 
-    /* Create CALL objects */
+    /* Asynchronous setup of in-call unsolicited events */
+    void     (* setup_in_call_unsolicited_events)        (MMIfaceModemVoice   *self,
+                                                          GAsyncReadyCallback  callback,
+                                                          gpointer             user_data);
+    gboolean (* setup_in_call_unsolicited_events_finish) (MMIfaceModemVoice   *self,
+                                                          GAsyncResult        *res,
+                                                          GError             **error);
+
+    /* Asynchronous cleanup of in-call unsolicited events */
+    void     (* cleanup_in_call_unsolicited_events)        (MMIfaceModemVoice   *self,
+                                                            GAsyncReadyCallback  callback,
+                                                            gpointer             user_data);
+    gboolean (* cleanup_in_call_unsolicited_events_finish) (MMIfaceModemVoice   *self,
+                                                            GAsyncResult        *res,
+                                                            GError             **error);
+
+    /* Asynchronous setup of in-call audio channel */
+    void     (* setup_in_call_audio_channel)        (MMIfaceModemVoice   *self,
+                                                     GAsyncReadyCallback  callback,
+                                                     gpointer             user_data);
+    gboolean (* setup_in_call_audio_channel_finish) (MMIfaceModemVoice   *self,
+                                                     GAsyncResult        *res,
+                                                     MMPort             **audio_port,   /* optional */
+                                                     MMCallAudioFormat  **audio_format, /* optional */
+                                                     GError             **error);
+
+    /* Asynchronous cleanup of in-call audio channel */
+    void     (* cleanup_in_call_audio_channel)        (MMIfaceModemVoice   *self,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+    gboolean (* cleanup_in_call_audio_channel_finish) (MMIfaceModemVoice   *self,
+                                                       GAsyncResult        *res,
+                                                       GError             **error);
+
+    /* Load full list of calls (MMCallInfo list) */
+    void     (* load_call_list)        (MMIfaceModemVoice    *self,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+    gboolean (* load_call_list_finish) (MMIfaceModemVoice    *self,
+                                        GAsyncResult         *res,
+                                        GList               **call_info_list,
+                                        GError              **error);
+
+    /* Create call objects */
     MMBaseCall * (* create_call) (MMIfaceModemVoice *self,
                                   MMCallDirection    direction,
                                   const gchar       *number);
+
+    /* Hold and accept */
+    void     (* hold_and_accept)        (MMIfaceModemVoice    *self,
+                                         GAsyncReadyCallback   callback,
+                                         gpointer              user_data);
+    gboolean (* hold_and_accept_finish) (MMIfaceModemVoice    *self,
+                                         GAsyncResult         *res,
+                                         GError              **error);
+
+    /* Hangup and accept */
+    void     (* hangup_and_accept)        (MMIfaceModemVoice    *self,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+    gboolean (* hangup_and_accept_finish) (MMIfaceModemVoice    *self,
+                                           GAsyncResult         *res,
+                                           GError              **error);
+
+    /* Hangup all */
+    void     (* hangup_all)        (MMIfaceModemVoice    *self,
+                                    GAsyncReadyCallback   callback,
+                                    gpointer              user_data);
+    gboolean (* hangup_all_finish) (MMIfaceModemVoice    *self,
+                                    GAsyncResult         *res,
+                                    GError              **error);
+
+    /* Join multiparty */
+    void     (* join_multiparty)        (MMIfaceModemVoice    *self,
+                                         GAsyncReadyCallback   callback,
+                                         gpointer              user_data);
+    gboolean (* join_multiparty_finish) (MMIfaceModemVoice    *self,
+                                         GAsyncResult         *res,
+                                         GError              **error);
+
+    /* Leave multiparty */
+    void     (* leave_multiparty)        (MMIfaceModemVoice    *self,
+                                          MMBaseCall           *call,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data);
+    gboolean (* leave_multiparty_finish) (MMIfaceModemVoice    *self,
+                                          GAsyncResult         *res,
+                                          GError              **error);
+
+    /* Transfer */
+    void     (* transfer)        (MMIfaceModemVoice    *self,
+                                  GAsyncReadyCallback   callback,
+                                  gpointer              user_data);
+    gboolean (* transfer_finish) (MMIfaceModemVoice    *self,
+                                  GAsyncResult         *res,
+                                  GError              **error);
+
+    /* Call waiting setup */
+    void     (* call_waiting_setup)        (MMIfaceModemVoice    *self,
+                                            gboolean              enable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+    gboolean (* call_waiting_setup_finish) (MMIfaceModemVoice    *self,
+                                            GAsyncResult         *res,
+                                            GError              **error);
+
+    /* Call waiting query */
+    void     (* call_waiting_query)        (MMIfaceModemVoice    *self,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+    gboolean (* call_waiting_query_finish) (MMIfaceModemVoice    *self,
+                                            GAsyncResult         *res,
+                                            gboolean             *status,
+                                            GError              **error);
 };
 
 GType mm_iface_modem_voice_get_type (void);
@@ -116,10 +228,40 @@
 
 /* Bind properties for simple GetStatus() */
 void mm_iface_modem_voice_bind_simple_status (MMIfaceModemVoice *self,
-                                              MMSimpleStatus *status);
+                                              MMSimpleStatus    *status);
 
-/* Incoming call management */
-void mm_iface_modem_voice_report_incoming_call (MMIfaceModemVoice *self,
-                                                const gchar       *number);
+/* Single call info reporting */
+void mm_iface_modem_voice_report_call (MMIfaceModemVoice *self,
+                                       const MMCallInfo  *call_info);
+
+/* Full current call list reporting (MMCallInfo list) */
+void mm_iface_modem_voice_report_all_calls (MMIfaceModemVoice *self,
+                                            GList             *call_info_list);
+
+/* Report an incoming DTMF received */
+void mm_iface_modem_voice_received_dtmf (MMIfaceModemVoice *self,
+                                         guint              index,
+                                         const gchar       *dtmf);
+
+/* Join/Leave multiparty calls
+ *
+ * These actions are provided in the Call API, but implemented in the
+ * modem Voice interface because they really affect multiple calls at
+ * the same time.
+ */
+void     mm_iface_modem_voice_join_multiparty         (MMIfaceModemVoice    *self,
+                                                       MMBaseCall           *call,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+gboolean mm_iface_modem_voice_join_multiparty_finish  (MMIfaceModemVoice    *self,
+                                                       GAsyncResult         *res,
+                                                       GError              **error);
+void     mm_iface_modem_voice_leave_multiparty        (MMIfaceModemVoice    *self,
+                                                       MMBaseCall           *call,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+gboolean mm_iface_modem_voice_leave_multiparty_finish (MMIfaceModemVoice    *self,
+                                                       GAsyncResult         *res,
+                                                       GError              **error);
 
 #endif /* MM_IFACE_MODEM_VOICE_H */
diff --git a/src/mm-iface-modem.c b/src/mm-iface-modem.c
index 66db38a..c284524 100644
--- a/src/mm-iface-modem.c
+++ b/src/mm-iface-modem.c
@@ -372,6 +372,7 @@
     g_assert (ctx->pin_check_timeout_id == 0);
     MM_IFACE_MODEM_GET_INTERFACE (self)->load_unlock_required (
         self,
+        (ctx->retries == MAX_RETRIES), /* last_attempt? */
         (GAsyncReadyCallback)load_unlock_required_ready,
         task);
 }
@@ -425,7 +426,7 @@
 
 /*****************************************************************************/
 
-static MMModemState get_current_consolidated_state (MMIfaceModem *self, MMModemState modem_state);
+static MMModemState get_consolidated_subsystem_state (MMIfaceModem *self);
 
 typedef struct {
     MMBaseBearer *self;
@@ -490,7 +491,7 @@
             new_state = MM_MODEM_STATE_DISCONNECTING;
             break;
         case MM_BEARER_STATUS_DISCONNECTED:
-            new_state = get_current_consolidated_state (self, MM_MODEM_STATE_UNKNOWN);
+            new_state = get_consolidated_subsystem_state (self);
             break;
         }
 
@@ -753,6 +754,7 @@
         return;
     }
 
+#if ! defined WITH_AT_COMMAND_VIA_DBUS
     /* If we are not in Debug mode, report an error */
     if (!mm_context_get_debug ()) {
         g_dbus_method_invocation_return_error (ctx->invocation,
@@ -763,6 +765,7 @@
         handle_command_context_free (ctx);
         return;
     }
+#endif
 
     /* If command is not implemented, report an error */
     if (!MM_IFACE_MODEM_GET_INTERFACE (self)->command ||
@@ -1518,7 +1521,7 @@
 
     /* Enabled may really be searching or registered */
     if (new_state == MM_MODEM_STATE_ENABLED)
-        new_state = get_current_consolidated_state (self, new_state);
+        new_state = get_consolidated_subsystem_state (self);
 
     /* Update state only if different */
     if (new_state != old_state) {
@@ -1612,9 +1615,12 @@
 }
 
 static MMModemState
-get_current_consolidated_state (MMIfaceModem *self, MMModemState modem_state)
+get_consolidated_subsystem_state (MMIfaceModem *self)
 {
-    MMModemState consolidated = modem_state;
+    /* Use as initial state ENABLED, which is the minimum one expected. Do not
+     * use the old modem state as initial state, as that would disallow reporting
+     * e.g. ENABLED if the old state was REGISTERED (as ENABLED < REGISTERED). */
+    MMModemState consolidated = MM_MODEM_STATE_ENABLED;
     GArray *subsystem_states;
 
     if (G_UNLIKELY (!state_update_context_quark))
@@ -1698,7 +1704,7 @@
         g_array_append_val (subsystem_states, s);
     }
 
-    return get_current_consolidated_state (self, modem_state);
+    return get_consolidated_subsystem_state (self);
 }
 
 void
@@ -2252,9 +2258,14 @@
 /*****************************************************************************/
 /* Current bands setting */
 
+#define AFTER_SET_LOAD_CURRENT_BANDS_RETRIES      5
+#define AFTER_SET_LOAD_CURRENT_BANDS_TIMEOUT_SECS 1
+
 typedef struct {
     MmGdbusModem *skeleton;
-    GArray *bands_array;
+    GArray       *bands_array;
+    GArray       *supported_bands_array; /* when ANY requested */
+    guint         retries;
 } SetCurrentBandsContext;
 
 static void
@@ -2264,6 +2275,8 @@
         g_object_unref (ctx->skeleton);
     if (ctx->bands_array)
         g_array_unref (ctx->bands_array);
+    if (ctx->supported_bands_array)
+        g_array_unref (ctx->supported_bands_array);
     g_slice_free (SetCurrentBandsContext, ctx);
 }
 
@@ -2284,48 +2297,116 @@
 
     /* Never show just 'any' in the interface */
     if (ctx->bands_array->len == 1 && g_array_index (ctx->bands_array, MMModemBand, 0) == MM_MODEM_BAND_ANY) {
-        GArray *supported_bands;
-
-        supported_bands = (mm_common_bands_variant_to_garray (mm_gdbus_modem_get_supported_bands (ctx->skeleton)));
-        mm_common_bands_garray_sort (supported_bands);
-        mm_gdbus_modem_set_current_bands (ctx->skeleton, mm_common_bands_garray_to_variant (supported_bands));
-        g_array_unref (supported_bands);
-    } else {
-        mm_common_bands_garray_sort (ctx->bands_array);
-        mm_gdbus_modem_set_current_bands (ctx->skeleton, mm_common_bands_garray_to_variant (ctx->bands_array));
+        g_assert (ctx->supported_bands_array);
+        g_array_unref (ctx->bands_array);
+        ctx->bands_array = g_array_ref (ctx->supported_bands_array);
     }
 
+    mm_common_bands_garray_sort (ctx->bands_array);
+    mm_gdbus_modem_set_current_bands (ctx->skeleton, mm_common_bands_garray_to_variant (ctx->bands_array));
+
     g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
 
+static void set_current_bands_reload_schedule (GTask *task);
+
 static void
 after_set_load_current_bands_ready (MMIfaceModem *self,
                                     GAsyncResult *res,
                                     GTask        *task)
 {
-    GError                 *error = NULL;
-    GArray                 *current_bands;
     SetCurrentBandsContext *ctx;
+    GArray                 *current_bands;
+    GError                 *error = NULL;
+    GArray                 *requested_bands = NULL;
 
     ctx = g_task_get_task_data (task);
 
     current_bands = MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_bands_finish (self, res, &error);
     if (!current_bands) {
-        /* Errors when getting bands won't be critical */
+        /* If we can retry, do it */
+        if (ctx->retries > 0) {
+            mm_dbg ("couldn't load current bands: '%s' (will retry)", error->message);
+            g_clear_error (&error);
+            set_current_bands_reload_schedule (task);
+            goto out;
+        }
+
+        /* Errors when reloading bands won't be critical */
         mm_warn ("couldn't load current bands: '%s'", error->message);
-        g_error_free (error);
-        /* Default to the ones we requested */
+        g_clear_error (&error);
         set_current_bands_complete_with_defaults (task);
-        return;
+        goto out;
     }
 
+    if ((ctx->bands_array->len == 1) && (g_array_index (ctx->bands_array, MMModemBand, 0) == MM_MODEM_BAND_ANY))
+        requested_bands = g_array_ref (ctx->supported_bands_array);
+    else
+        requested_bands = g_array_ref (ctx->bands_array);
+
+    /* Compare arrays */
+    if (!mm_common_bands_garray_cmp (current_bands, requested_bands)) {
+        gchar *requested_str;
+        gchar *current_str;
+
+        /* If we can retry, do it */
+        if (ctx->retries > 0) {
+            mm_dbg ("reloaded current bands different to the requested ones (will retry)");
+            set_current_bands_reload_schedule (task);
+            goto out;
+        }
+
+        requested_str = mm_common_build_bands_string ((MMModemBand *)requested_bands->data, requested_bands->len);
+        current_str   = mm_common_build_bands_string ((MMModemBand *)current_bands->data, current_bands->len);
+        error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                             "reloaded current bands (%s) different to the requested ones (%s)",
+                             current_str, requested_str);
+        g_free (requested_str);
+        g_free (current_str);
+    }
+
+    /* Store as current the last loaded ones and set operation result */
     mm_common_bands_garray_sort (current_bands);
     mm_gdbus_modem_set_current_bands (ctx->skeleton, mm_common_bands_garray_to_variant (current_bands));
-    g_array_unref (current_bands);
 
-    g_task_return_boolean (task, TRUE);
+    if (error)
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
     g_object_unref (task);
+
+ out:
+    g_array_unref (requested_bands);
+    g_array_unref (current_bands);
+}
+
+static gboolean
+set_current_bands_reload (GTask *task)
+{
+    MMIfaceModem           *self;
+    SetCurrentBandsContext *ctx;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    g_assert (ctx->retries > 0);
+    ctx->retries--;
+
+    MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_bands (
+        self,
+        (GAsyncReadyCallback)after_set_load_current_bands_ready,
+        task);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+set_current_bands_reload_schedule (GTask *task)
+{
+    g_timeout_add_seconds (AFTER_SET_LOAD_CURRENT_BANDS_TIMEOUT_SECS,
+                           (GSourceFunc) set_current_bands_reload,
+                           task);
 }
 
 static void
@@ -2343,10 +2424,7 @@
 
     if (MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_bands &&
         MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_bands_finish) {
-        MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_bands (
-            self,
-            (GAsyncReadyCallback)after_set_load_current_bands_ready,
-            task);
+        set_current_bands_reload (task);
         return;
     }
 
@@ -2419,7 +2497,6 @@
                                   gpointer user_data)
 {
     SetCurrentBandsContext *ctx;
-    GArray *supported_bands_array;
     GArray *current_bands_array;
     GError *error = NULL;
     gchar *bands_string;
@@ -2440,6 +2517,7 @@
 
     /* Setup context */
     ctx = g_slice_new0 (SetCurrentBandsContext);
+    ctx->retries = AFTER_SET_LOAD_CURRENT_BANDS_RETRIES;
 
     task = g_task_new (self, NULL, callback, user_data);
     g_task_set_task_data (task, ctx, (GDestroyNotify)set_current_bands_context_free);
@@ -2460,8 +2538,8 @@
                                                  bands_array->len);
 
     /* Get list of supported bands */
-    supported_bands_array = (mm_common_bands_variant_to_garray (
-                                 mm_gdbus_modem_get_supported_bands (ctx->skeleton)));
+    ctx->supported_bands_array = (mm_common_bands_variant_to_garray (
+                                      mm_gdbus_modem_get_supported_bands (ctx->skeleton)));
 
     /* Set ctx->bands_array to target list of bands before comparing with current list
      * of bands. If input list of bands contains only ANY, target list of bands is set
@@ -2470,8 +2548,8 @@
         g_array_index (bands_array, MMModemBand, 0) == MM_MODEM_BAND_ANY) {
         guint i;
 
-        for (i = 0; i < supported_bands_array->len; i++) {
-            MMModemBand band = g_array_index (supported_bands_array, MMModemBand, i);
+        for (i = 0; i < ctx->supported_bands_array->len; i++) {
+            MMModemBand band = g_array_index (ctx->supported_bands_array, MMModemBand, i);
 
             if (band != MM_MODEM_BAND_ANY &&
                 band != MM_MODEM_BAND_UNKNOWN) {
@@ -2479,7 +2557,7 @@
                     ctx->bands_array = g_array_sized_new (FALSE,
                                                           FALSE,
                                                           sizeof (MMModemBand),
-                                                          supported_bands_array->len);
+                                                          ctx->supported_bands_array->len);
                 g_array_append_val (ctx->bands_array, band);
             }
         }
@@ -2495,7 +2573,6 @@
         mm_dbg ("Requested list of bands (%s) is equal to the current ones, skipping re-set",
                 bands_string);
         g_free (bands_string);
-        g_array_unref (supported_bands_array);
         g_array_unref (current_bands_array);
         g_task_return_boolean (task, TRUE);
         g_object_unref (task);
@@ -2510,13 +2587,12 @@
     }
 
     /* Validate input list of bands */
-    if (!validate_bands (supported_bands_array,
+    if (!validate_bands (ctx->supported_bands_array,
                          ctx->bands_array,
                          &error)) {
         mm_dbg ("Requested list of bands (%s) cannot be handled",
                 bands_string);
         g_free (bands_string);
-        g_array_unref (supported_bands_array);
         g_array_unref (current_bands_array);
         g_task_return_error (task, error);
         g_object_unref (task);
@@ -2530,7 +2606,6 @@
         (GAsyncReadyCallback)set_current_bands_ready,
         task);
 
-    g_array_unref (supported_bands_array);
     g_array_unref (current_bands_array);
     g_free (bands_string);
 }
@@ -2622,10 +2697,14 @@
 /*****************************************************************************/
 /* Set current modes */
 
+#define AFTER_SET_LOAD_CURRENT_MODES_RETRIES      5
+#define AFTER_SET_LOAD_CURRENT_MODES_TIMEOUT_SECS 1
+
 typedef struct {
     MmGdbusModem *skeleton;
-    MMModemMode allowed;
-    MMModemMode preferred;
+    MMModemMode   allowed;
+    MMModemMode   preferred;
+    guint         retries;
 } SetCurrentModesContext;
 
 static void
@@ -2633,7 +2712,7 @@
 {
     if (ctx->skeleton)
         g_object_unref (ctx->skeleton);
-    g_free (ctx);
+    g_slice_free (SetCurrentModesContext, ctx);
 }
 
 gboolean
@@ -2644,6 +2723,8 @@
     return g_task_propagate_boolean (G_TASK (res), error);
 }
 
+static void set_current_modes_reload_schedule (GTask *task);
+
 static void
 after_set_load_current_modes_ready (MMIfaceModem *self,
                                     GAsyncResult *res,
@@ -2661,20 +2742,94 @@
                                                                          &allowed,
                                                                          &preferred,
                                                                          &error)) {
+        /* If we can retry, do it */
+        if (ctx->retries > 0) {
+            mm_dbg ("couldn't load current allowed/preferred modes: '%s'", error->message);
+            g_error_free (error);
+            set_current_modes_reload_schedule (task);
+            return;
+        }
+
         /* Errors when getting allowed/preferred won't be critical */
         mm_warn ("couldn't load current allowed/preferred modes: '%s'", error->message);
-        g_error_free (error);
+        g_clear_error (&error);
 
         /* If errors getting allowed modes, default to the ones we asked for */
         mm_gdbus_modem_set_current_modes (ctx->skeleton, g_variant_new ("(uu)", ctx->allowed, ctx->preferred));
-    } else
-        mm_gdbus_modem_set_current_modes (ctx->skeleton, g_variant_new ("(uu)", allowed, preferred));
+        goto out;
+    }
 
-    /* Done */
-    g_task_return_boolean (task, TRUE);
+    /* Store as current the last loaded ones and set operation result */
+    mm_gdbus_modem_set_current_modes (ctx->skeleton, g_variant_new ("(uu)", allowed, preferred));
+
+    /* Compare modes. If the requested one was ANY, we won't consider an error if the
+     * result differs. */
+    if (((allowed != ctx->allowed) || (preferred != ctx->preferred)) && (ctx->allowed != MM_MODEM_MODE_ANY)) {
+        gchar *requested_allowed_str;
+        gchar *requested_preferred_str;
+        gchar *current_allowed_str;
+        gchar *current_preferred_str;
+
+        /* If we can retry, do it */
+        if (ctx->retries > 0) {
+            mm_dbg ("reloaded current modes different to the requested ones (will retry)");
+            set_current_modes_reload_schedule (task);
+            return;
+        }
+
+        requested_allowed_str = mm_modem_mode_build_string_from_mask (ctx->allowed);
+        requested_preferred_str = mm_modem_mode_build_string_from_mask (ctx->preferred);
+        current_allowed_str = mm_modem_mode_build_string_from_mask (allowed);
+        current_preferred_str = mm_modem_mode_build_string_from_mask (preferred);
+
+        error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                             "reloaded modes (allowed '%s' and preferred '%s') different "
+                             "to the requested ones (allowed '%s' and preferred '%s')",
+                             requested_allowed_str, requested_preferred_str,
+                             current_allowed_str, current_preferred_str);
+
+        g_free (requested_allowed_str);
+        g_free (requested_preferred_str);
+        g_free (current_allowed_str);
+        g_free (current_preferred_str);
+    }
+
+out:
+    if (error)
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
 
+static gboolean
+set_current_modes_reload (GTask *task)
+{
+    MMIfaceModem           *self;
+    SetCurrentModesContext *ctx;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    g_assert (ctx->retries > 0);
+    ctx->retries--;
+
+    MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_modes (
+        self,
+        (GAsyncReadyCallback)after_set_load_current_modes_ready,
+        task);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+set_current_modes_reload_schedule (GTask *task)
+{
+    g_timeout_add_seconds (AFTER_SET_LOAD_CURRENT_MODES_TIMEOUT_SECS,
+                           (GSourceFunc) set_current_modes_reload,
+                           task);
+}
+
 static void
 set_current_modes_ready (MMIfaceModem *self,
                          GAsyncResult *res,
@@ -2691,10 +2846,7 @@
 
     if (MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_modes &&
         MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_modes_finish) {
-        MM_IFACE_MODEM_GET_INTERFACE (self)->load_current_modes (
-            self,
-            (GAsyncReadyCallback)after_set_load_current_modes_ready,
-            task);
+        set_current_modes_reload (task);
         return;
     }
 
@@ -2738,7 +2890,8 @@
     }
 
     /* Setup context */
-    ctx = g_new0 (SetCurrentModesContext, 1);
+    ctx = g_slice_new0 (SetCurrentModesContext);
+    ctx->retries = AFTER_SET_LOAD_CURRENT_MODES_RETRIES;
     ctx->allowed = allowed;
     ctx->preferred = preferred;
 
diff --git a/src/mm-iface-modem.h b/src/mm-iface-modem.h
index 51cb02b..9df3903 100644
--- a/src/mm-iface-modem.h
+++ b/src/mm-iface-modem.h
@@ -120,6 +120,7 @@
 
     /* Loading of the UnlockRequired property */
     void (*load_unlock_required) (MMIfaceModem *self,
+                                  gboolean last_attempt,
                                   GAsyncReadyCallback callback,
                                   gpointer user_data);
     MMModemLock (*load_unlock_required_finish) (MMIfaceModem *self,
diff --git a/src/mm-modem-helpers-qmi.c b/src/mm-modem-helpers-qmi.c
index 86e1803..ef6e048 100644
--- a/src/mm-modem-helpers-qmi.c
+++ b/src/mm-modem-helpers-qmi.c
@@ -275,6 +275,13 @@
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_21, MM_MODEM_BAND_EUTRAN_21 },
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_24, MM_MODEM_BAND_EUTRAN_24 },
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_25, MM_MODEM_BAND_EUTRAN_25 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_26, MM_MODEM_BAND_EUTRAN_26 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_27, MM_MODEM_BAND_EUTRAN_27 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_28, MM_MODEM_BAND_EUTRAN_28 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_29, MM_MODEM_BAND_EUTRAN_29 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_30, MM_MODEM_BAND_EUTRAN_30 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_31, MM_MODEM_BAND_EUTRAN_31 },
+    { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_32, MM_MODEM_BAND_EUTRAN_32 },
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_33, MM_MODEM_BAND_EUTRAN_33 },
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_34, MM_MODEM_BAND_EUTRAN_34 },
     { QMI_DMS_LTE_BAND_CAPABILITY_EUTRAN_35, MM_MODEM_BAND_EUTRAN_35 },
@@ -472,6 +479,13 @@
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_21, MM_MODEM_BAND_EUTRAN_21 },
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_24, MM_MODEM_BAND_EUTRAN_24 },
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_25, MM_MODEM_BAND_EUTRAN_25 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_26, MM_MODEM_BAND_EUTRAN_26 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_27, MM_MODEM_BAND_EUTRAN_27 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_28, MM_MODEM_BAND_EUTRAN_28 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_29, MM_MODEM_BAND_EUTRAN_29 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_30, MM_MODEM_BAND_EUTRAN_30 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_31, MM_MODEM_BAND_EUTRAN_31 },
+    { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_32, MM_MODEM_BAND_EUTRAN_32 },
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_33, MM_MODEM_BAND_EUTRAN_33 },
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_34, MM_MODEM_BAND_EUTRAN_34 },
     { QMI_NAS_LTE_BAND_PREFERENCE_EUTRAN_35, MM_MODEM_BAND_EUTRAN_35 },
@@ -567,7 +581,8 @@
 
     *qmi_bands = 0;
     *qmi_lte_bands = 0;
-    memset (extended_qmi_lte_bands, 0, extended_qmi_lte_bands_size * sizeof (guint64));
+    if (extended_qmi_lte_bands)
+        memset (extended_qmi_lte_bands, 0, extended_qmi_lte_bands_size * sizeof (guint64));
 
     for (i = 0; i < mm_bands->len; i++) {
         MMModemBand band;
diff --git a/src/mm-modem-helpers.c b/src/mm-modem-helpers.c
index cf008d7..f7115cb 100644
--- a/src/mm-modem-helpers.c
+++ b/src/mm-modem-helpers.c
@@ -544,16 +544,152 @@
 GRegex *
 mm_voice_clip_regex_get (void)
 {
-    /* Example:
-     * <CR><LF>+CLIP: "+393351391306",145,,,,0<CR><LF>
-     *                 \_ Number      \_ Type \_ Validity
+    /*
+     * Only first 2 fields are mandatory:
+     *   +CLIP: <number>,<type>[,<subaddr>,<satype>[,[<alpha>][,<CLI_validity>]]]
+     *
+     * Example:
+     *   <CR><LF>+CLIP: "+393351391306",145,,,,0<CR><LF>
+     *                   \_ Number      \_ Type
      */
-    return g_regex_new ("\\r\\n\\+CLIP:\\s*(\\S+),\\s*(\\d+),\\s*,\\s*,\\s*,\\s*(\\d+)\\r\\n",
+    return g_regex_new ("\\r\\n\\+CLIP:\\s*([^,\\s]*)\\s*,\\s*(\\d+)\\s*,?(.*)\\r\\n",
                         G_REGEX_RAW | G_REGEX_OPTIMIZE,
                         0,
                         NULL);
 }
 
+GRegex *
+mm_voice_ccwa_regex_get (void)
+{
+    /*
+     * Only first 3 fields are mandatory, but we read only the first one
+     *   +CCWA: <number>,<type>,<class>,[<alpha>][,<CLI_validity>[,<subaddr>,<satype>[,<priority>]]]
+     *
+     * Example:
+     *   <CR><LF>+CCWA: "+393351391306",145,1
+     *                   \_ Number      \_ Type
+     */
+    return g_regex_new ("\\r\\n\\+CCWA:\\s*([^,\\s]*)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,?(.*)\\r\\n",
+                        G_REGEX_RAW | G_REGEX_OPTIMIZE,
+                        0,
+                        NULL);
+}
+
+static void
+call_info_free (MMCallInfo *info)
+{
+    if (!info)
+        return;
+    g_free (info->number);
+    g_slice_free (MMCallInfo, info);
+}
+
+gboolean
+mm_3gpp_parse_clcc_response (const gchar  *str,
+                             GList       **out_list,
+                             GError      **error)
+{
+    GRegex     *r;
+    GList      *list = NULL;
+    GError     *inner_error = NULL;
+    GMatchInfo *match_info  = NULL;
+
+    static const MMCallDirection call_direction[] = {
+        [0] = MM_CALL_DIRECTION_OUTGOING,
+        [1] = MM_CALL_DIRECTION_INCOMING,
+    };
+
+    static const MMCallState call_state[] = {
+        [0] = MM_CALL_STATE_ACTIVE,
+        [1] = MM_CALL_STATE_HELD,
+        [2] = MM_CALL_STATE_DIALING,     /* Dialing  (MOC) */
+        [3] = MM_CALL_STATE_RINGING_OUT, /* Alerting (MOC) */
+        [4] = MM_CALL_STATE_RINGING_IN,  /* Incoming (MTC) */
+        [5] = MM_CALL_STATE_WAITING,     /* Waiting  (MTC) */
+    };
+
+    g_assert (out_list);
+
+    /*
+     *         1     2     3      4      5       6        7       8        9           10
+     *  +CLCC: <idx>,<dir>,<stat>,<mode>,<mpty>[,<number>,<type>[,<alpha>[,<priority>[,<CLI validity>]]]]
+     *  +CLCC: <idx>,<dir>,<stat>,<mode>,<mpty>[,<number>,<type>[,<alpha>[,<priority>[,<CLI validity>]]]]
+     *  ...
+     */
+
+    r = g_regex_new ("\\+CLCC:\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+)" /* mandatory fields */
+                     "(?:,\\s*([^,]*),\\s*(\\d+)"                                     /* number and type */
+                     "(?:,\\s*([^,]*)"                                                /* alpha */
+                     "(?:,\\s*(\\d*)"                                                 /* priority */
+                     "(?:,\\s*(\\d*)"                                                 /* CLI validity */
+                     ")?)?)?)?$",
+                     G_REGEX_RAW | G_REGEX_MULTILINE | G_REGEX_NEWLINE_CRLF,
+                     G_REGEX_MATCH_NEWLINE_CRLF,
+                     NULL);
+    g_assert (r != NULL);
+
+    g_regex_match_full (r, str, strlen (str), 0, 0, &match_info, &inner_error);
+    if (inner_error)
+        goto out;
+
+    /* Parse the results */
+    while (g_match_info_matches (match_info)) {
+        MMCallInfo *call_info;
+        guint       aux;
+
+        call_info = g_slice_new0 (MMCallInfo);
+
+        if (!mm_get_uint_from_match_info (match_info, 1, &call_info->index)) {
+            mm_warn ("couldn't parse call index from +CLCC line");
+            goto next;
+        }
+
+        if (!mm_get_uint_from_match_info (match_info, 2, &aux) ||
+            (aux >= G_N_ELEMENTS (call_direction))) {
+            mm_warn ("couldn't parse call direction from +CLCC line");
+            goto next;
+        }
+        call_info->direction = call_direction[aux];
+
+        if (!mm_get_uint_from_match_info (match_info, 3, &aux) ||
+            (aux >= G_N_ELEMENTS (call_state))) {
+            mm_warn ("couldn't parse call state from +CLCC line");
+            goto next;
+        }
+        call_info->state = call_state[aux];
+
+        if (g_match_info_get_match_count (match_info) >= 7)
+            call_info->number = mm_get_string_unquoted_from_match_info (match_info, 6);
+
+        list = g_list_append (list, call_info);
+        call_info = NULL;
+
+    next:
+        call_info_free (call_info);
+        g_match_info_next (match_info, NULL);
+    }
+
+out:
+    g_clear_pointer (&match_info, g_match_info_free);
+    g_regex_unref (r);
+
+    if (inner_error) {
+        mm_3gpp_call_info_list_free (list);
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    *out_list = list;
+
+    return TRUE;
+}
+
+void
+mm_3gpp_call_info_list_free (GList *call_info_list)
+{
+    g_list_free_full (call_info_list, (GDestroyNotify) call_info_free);
+}
+
 /*************************************************************************/
 
 static MMFlowControl
@@ -1404,6 +1540,131 @@
 
 /*************************************************************************/
 
+static guint
+find_max_allowed_cid (GList            *context_format_list,
+                      MMBearerIpFamily  ip_family)
+{
+    GList *l;
+
+    for (l = context_format_list; l; l = g_list_next (l)) {
+        MM3gppPdpContextFormat *format = l->data;
+
+        /* Found exact PDP type? */
+        if (format->pdp_type == ip_family)
+            return format->max_cid;
+    }
+    return 0;
+}
+
+guint
+mm_3gpp_select_best_cid (const gchar      *apn,
+                         MMBearerIpFamily  ip_family,
+                         GList            *context_list,
+                         GList            *context_format_list,
+                         gboolean         *cid_reused,
+                         gboolean         *cid_overwritten)
+{
+    GList *l;
+    guint  prev_cid = 0;
+    guint  exact_cid = 0;
+    guint  unused_cid = 0;
+    guint  max_cid = 0;
+    guint  max_allowed_cid = 0;
+    guint  blank_cid = 0;
+    gchar *ip_family_str;
+
+    ip_family_str = mm_bearer_ip_family_build_string_from_mask (ip_family);
+    mm_dbg ("Looking for best CID matching APN '%s' and PDP type '%s'...",
+            apn, ip_family_str);
+    g_free (ip_family_str);
+
+    /* Look for the exact PDP context we want */
+    for (l = context_list; l; l = g_list_next (l)) {
+        MM3gppPdpContext *pdp = l->data;
+
+        /* Match PDP type */
+        if (pdp->pdp_type == ip_family) {
+            /* Try to match exact APN and PDP type */
+            if (mm_3gpp_cmp_apn_name (apn, pdp->apn)) {
+                exact_cid = pdp->cid;
+                break;
+            }
+
+            /* Same PDP type but with no APN set? we may use that one if no exact match found */
+            if ((!pdp->apn || !pdp->apn[0]) && !blank_cid)
+                blank_cid = pdp->cid;
+        }
+
+        /* If an unused CID was not found yet and the previous CID is not (CID - 1),
+         * this means that (previous CID + 1) is an unused CID that can be used.
+         * This logic will allow us using unused CIDs that are available in the gaps
+         * between already defined contexts.
+         */
+        if (!unused_cid && prev_cid && ((prev_cid + 1) < pdp->cid))
+            unused_cid = prev_cid + 1;
+
+        /* Update previous CID value to the current CID for use in the next loop,
+         * unless an unused CID was already found.
+         */
+        if (!unused_cid)
+            prev_cid = pdp->cid;
+
+        /* Update max CID if we found a bigger one */
+        if (max_cid < pdp->cid)
+            max_cid = pdp->cid;
+    }
+
+    /* Always prefer an exact match */
+    if (exact_cid) {
+        mm_dbg ("Found exact context at CID %u", exact_cid);
+        *cid_reused = TRUE;
+        *cid_overwritten = FALSE;
+        return exact_cid;
+    }
+
+    /* Try to use an unused CID detected in between the already defined contexts */
+    if (unused_cid) {
+        mm_dbg ("Found unused context at CID %u", unused_cid);
+        *cid_reused = FALSE;
+        *cid_overwritten = FALSE;
+        return unused_cid;
+    }
+
+    /* If the max existing CID found during CGDCONT? is below the max allowed
+     * CID, then we can use the next available CID because it's an unused one. */
+    max_allowed_cid = find_max_allowed_cid (context_format_list, ip_family);
+    if (max_cid && (max_cid < max_allowed_cid)) {
+        mm_dbg ("Found unused context at CID %u (<%u)", max_cid + 1, max_allowed_cid);
+        *cid_reused = FALSE;
+        *cid_overwritten = FALSE;
+        return (max_cid + 1);
+    }
+
+    /* Rewrite a context defined with no APN, if any */
+    if (blank_cid) {
+        mm_dbg ("Rewriting context with empty APN at CID %u", blank_cid);
+        *cid_reused = FALSE;
+        *cid_overwritten = TRUE;
+        return blank_cid;
+    }
+
+    /* Rewrite the last existing one found */
+    if (max_cid) {
+        mm_dbg ("Rewriting last context detected at CID %u", max_cid);
+        *cid_reused = FALSE;
+        *cid_overwritten = TRUE;
+        return max_cid;
+    }
+
+    /* Otherwise, just fallback to CID=1 */
+    mm_dbg ("Falling back to CID 1");
+    *cid_reused = FALSE;
+    *cid_overwritten = TRUE;
+    return 1;
+}
+
+/*************************************************************************/
+
 static void
 mm_3gpp_pdp_context_format_free (MM3gppPdpContextFormat *format)
 {
@@ -1876,7 +2137,7 @@
     if (!r)
         return FALSE;
 
-    if (!g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL)) {
+    if (!g_regex_match (r, reply, 0, &match_info)) {
         g_set_error (error,
                      MM_CORE_ERROR,
                      MM_CORE_ERROR_FAILED,
@@ -1927,7 +2188,7 @@
     r = g_regex_new ("\\+CMGR:\\s*(\\d+)\\s*,([^,]*),\\s*(\\d+)\\s*([^\\r\\n]*)", 0, 0, NULL);
     g_assert (r);
 
-    if (!g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL)) {
+    if (!g_regex_match (r, reply, 0, &match_info)) {
         g_set_error (error,
                      MM_CORE_ERROR,
                      MM_CORE_ERROR_FAILED,
@@ -2009,7 +2270,7 @@
                      G_REGEX_RAW, 0, NULL);
     g_assert (r != NULL);
 
-    if (g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL) &&
+    if (g_regex_match (r, reply, 0, &match_info) &&
         mm_get_uint_from_match_info (match_info, 1, sw1) &&
         mm_get_uint_from_match_info (match_info, 2, sw2))
         *hex = mm_get_string_unquoted_from_match_info (match_info, 3);
@@ -2588,6 +2849,85 @@
 }
 
 /*************************************************************************/
+/* CCWA service query response parser */
+
+gboolean
+mm_3gpp_parse_ccwa_service_query_response (const gchar  *response,
+                                           gboolean     *status,
+                                           GError      **error)
+{
+    GRegex     *r;
+    GError     *inner_error = NULL;
+    GMatchInfo *match_info  = NULL;
+    gint        class_1_status = -1;
+
+    /*
+     * AT+CCWA=<n>[,<mode>]
+     *   +CCWA: <status>,<class1>
+     *   [+CCWA: <status>,<class2>
+     *   [...]]
+     *   OK
+     *
+     * If <classX> is 255 it applies to ALL classes.
+     *
+     * We're only interested in class 1 (voice)
+     */
+    r = g_regex_new ("\\+CCWA:\\s*(\\d+),\\s*(\\d+)$",
+                     G_REGEX_RAW | G_REGEX_MULTILINE | G_REGEX_NEWLINE_CRLF,
+                     G_REGEX_MATCH_NEWLINE_CRLF,
+                     NULL);
+    g_assert (r != NULL);
+
+    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
+    if (inner_error)
+        goto out;
+
+    /* Parse the results */
+    while (g_match_info_matches (match_info)) {
+        guint st;
+        guint class;
+
+        if (!mm_get_uint_from_match_info (match_info, 2, &class))
+            mm_warn ("couldn't parse class from +CCWA line");
+        else if (class == 1 || class == 255) {
+            if (!mm_get_uint_from_match_info (match_info, 1, &st))
+                mm_warn ("couldn't parse status from +CCWA line");
+            else {
+                class_1_status = st;
+                break;
+            }
+        }
+        g_match_info_next (match_info, NULL);
+    }
+
+out:
+    g_clear_pointer (&match_info, g_match_info_free);
+    g_regex_unref (r);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    if (class_1_status < 0) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND,
+                     "call waiting status for voice class missing");
+        return FALSE;
+    }
+
+    if (class_1_status != 0 && class_1_status != 1) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
+                     "call waiting status for voice class invalid: %d", class_1_status);
+        return FALSE;
+    }
+
+    if (status)
+        *status = (gboolean) class_1_status;
+
+    return TRUE;
+}
+
+/*************************************************************************/
 
 static MMSmsStorage
 storage_from_str (const gchar *str)
@@ -2648,7 +2988,7 @@
         array = g_array_new (FALSE, FALSE, sizeof (MMSmsStorage));
 
         /* Got a range group to match */
-        if (g_regex_match_full (r, split[i], strlen (split[i]), 0, 0, &match_info, NULL)) {
+        if (g_regex_match (r, split[i], 0, &match_info)) {
             while (g_match_info_matches (match_info)) {
                 gchar *str;
 
@@ -2812,7 +3152,7 @@
     if (!r)
         return FALSE;
 
-    if (g_regex_match_full (r, p, strlen (p), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, p, 0, &match_info)) {
         while (g_match_info_matches (match_info)) {
             str = g_match_info_fetch (match_info, 1);
             charsets |= mm_modem_charset_from_string (str);
@@ -2854,7 +3194,7 @@
     g_assert (r != NULL);
 
     *out_facilities = MM_MODEM_3GPP_FACILITY_NONE;
-    if (g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, reply, 0, &match_info)) {
         while (g_match_info_matches (match_info)) {
             gchar *str;
 
@@ -3171,7 +3511,7 @@
 
     hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) cind_response_free);
 
-    if (g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL)) {
+    if (g_regex_match (r, reply, 0, &match_info)) {
         while (g_match_info_matches (match_info)) {
             MM3gppCindResponse *resp;
             gchar *desc, *tmp;
@@ -3228,7 +3568,7 @@
     r = g_regex_new ("(\\d+)[^0-9]+", G_REGEX_UNGREEDY, 0, NULL);
     g_assert (r != NULL);
 
-    if (!g_regex_match_full (r, reply, strlen (reply), 0, 0, &match_info, NULL)) {
+    if (!g_regex_match (r, reply, 0, &match_info)) {
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not parse the +CIND response '%s': didn't match",
                      reply);
diff --git a/src/mm-modem-helpers.h b/src/mm-modem-helpers.h
index 237046a..73a13dd 100644
--- a/src/mm-modem-helpers.h
+++ b/src/mm-modem-helpers.h
@@ -93,9 +93,23 @@
 /*****************************************************************************/
 /* VOICE specific helpers and utilities */
 /*****************************************************************************/
+
 GRegex *mm_voice_ring_regex_get  (void);
 GRegex *mm_voice_cring_regex_get (void);
 GRegex *mm_voice_clip_regex_get  (void);
+GRegex *mm_voice_ccwa_regex_get  (void);
+
+/* +CLCC response parser */
+typedef struct {
+    guint            index;
+    MMCallDirection  direction;
+    MMCallState      state;
+    gchar           *number; /* optional */
+} MMCallInfo;
+gboolean mm_3gpp_parse_clcc_response (const gchar  *str,
+                                      GList       **out_list,
+                                      GError      **error);
+void     mm_3gpp_call_info_list_free (GList        *call_info_list);
 
 /*****************************************************************************/
 /* SERIAL specific helpers and utilities */
@@ -178,6 +192,14 @@
 GList *mm_3gpp_parse_cgdcont_read_response (const gchar *reply,
                                             GError **error);
 
+/* Select best CID to use during connection */
+guint mm_3gpp_select_best_cid (const gchar      *apn,
+                               MMBearerIpFamily  ip_family,
+                               GList            *context_list,
+                               GList            *context_format_list,
+                               gboolean         *cid_reused,
+                               gboolean         *cid_overwritten);
+
 /* AT+CGACT? (active PDP context query) response parser */
 typedef struct {
     guint cid;
@@ -383,6 +405,12 @@
                                                MMModem3gppEpsUeModeOperation  *out_mode,
                                                GError                        **error);
 
+/* CCWA service query response parser */
+gboolean mm_3gpp_parse_ccwa_service_query_response (const gchar  *response,
+                                                    gboolean     *status,
+                                                    GError      **error);
+
+
 /* Additional 3GPP-specific helpers */
 
 MMModem3gppFacility mm_3gpp_acronym_to_facility (const gchar *str);
@@ -403,29 +431,19 @@
 
 char *mm_3gpp_parse_iccid (const char *raw_iccid, GError **error);
 
-gboolean
-mm_3gpp_rscp_level_to_rscp (guint    rscp_level,
-                            gdouble *out_rscp);
 
-gboolean
-mm_3gpp_rxlev_to_rssi (guint    rxlev,
-                       gdouble *out_rssi);
-
-gboolean
-mm_3gpp_ecn0_level_to_ecio (guint    ecn0_level,
-                            gdouble *out_ecio);
-
-gboolean
-mm_3gpp_rsrq_level_to_rsrq (guint    rsrq_level,
-                            gdouble *out_rsrq);
-
-gboolean
-mm_3gpp_rsrp_level_to_rsrp (guint    rsrp_level,
-                            gdouble *out_rsrp);
-
-gboolean
-mm_3gpp_rssnr_level_to_rssnr (gint     rssnr_level,
-                              gdouble *out_rssnr);
+gboolean mm_3gpp_rscp_level_to_rscp   (guint    rscp_level,
+                                       gdouble *out_rscp);
+gboolean mm_3gpp_rxlev_to_rssi        (guint    rxlev,
+                                       gdouble *out_rssi);
+gboolean mm_3gpp_ecn0_level_to_ecio   (guint    ecn0_level,
+                                       gdouble *out_ecio);
+gboolean mm_3gpp_rsrq_level_to_rsrq   (guint    rsrq_level,
+                                       gdouble *out_rsrq);
+gboolean mm_3gpp_rsrp_level_to_rsrp   (guint    rsrp_level,
+                                       gdouble *out_rsrp);
+gboolean mm_3gpp_rssnr_level_to_rssnr (gint     rssnr_level,
+                                       gdouble *out_rssnr);
 
 /*****************************************************************************/
 /* CDMA specific helpers and utilities */
diff --git a/src/mm-plugin-manager.c b/src/mm-plugin-manager.c
index 2123bde..f0045af 100644
--- a/src/mm-plugin-manager.c
+++ b/src/mm-plugin-manager.c
@@ -1496,6 +1496,36 @@
 
 /*****************************************************************************/
 
+static void
+register_plugin_whitelist_tags (MMPluginManager *self,
+                                MMPlugin        *plugin)
+{
+    const gchar **tags;
+    guint         i;
+
+    if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_WHITELIST))
+        return;
+
+    tags = mm_plugin_get_allowed_udev_tags (plugin);
+    for (i = 0; tags && tags[i]; i++)
+        mm_filter_register_plugin_whitelist_tag (self->priv->filter, tags[i]);
+}
+
+static void
+register_plugin_whitelist_product_ids (MMPluginManager *self,
+                                       MMPlugin        *plugin)
+{
+    const mm_uint16_pair *product_ids;
+    guint                 i;
+
+    if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_WHITELIST))
+        return;
+
+    product_ids = mm_plugin_get_allowed_product_ids (plugin);
+    for (i = 0; product_ids && product_ids[i].l; i++)
+        mm_filter_register_plugin_whitelist_product_id (self->priv->filter, product_ids[i].l, product_ids[i].r);
+}
+
 static MMPlugin *
 load_plugin (const gchar *path)
 {
@@ -1609,6 +1639,10 @@
         else
             /* Vendor specific plugin */
             self->priv->plugins = g_list_append (self->priv->plugins, plugin);
+
+        /* Register plugin whitelist rules in filter, if any */
+        register_plugin_whitelist_tags (self, plugin);
+        register_plugin_whitelist_product_ids (self, plugin);
     }
 
     /* Check the generic plugin once all looped */
diff --git a/src/mm-plugin.c b/src/mm-plugin.c
index 08fe2cd..7150b03 100644
--- a/src/mm-plugin.c
+++ b/src/mm-plugin.c
@@ -37,6 +37,13 @@
 #include "mm-log.h"
 #include "mm-daemon-enums-types.h"
 
+#if defined WITH_QMI
+# include "mm-broadband-modem-qmi.h"
+#endif
+#if defined WITH_MBIM
+# include "mm-broadband-modem-mbim.h"
+#endif
+
 G_DEFINE_TYPE (MMPlugin, mm_plugin, G_TYPE_OBJECT)
 
 /* Virtual port corresponding to the embedded modem */
@@ -134,6 +141,18 @@
     return self->priv->name;
 }
 
+const gchar **
+mm_plugin_get_allowed_udev_tags (MMPlugin *self)
+{
+    return (const gchar **) self->priv->udev_tags;
+}
+
+const mm_uint16_pair *
+mm_plugin_get_allowed_product_ids (MMPlugin *self)
+{
+    return self->priv->product_ids;
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -920,9 +939,22 @@
 
         /* Grab each port */
         for (l = port_probes; l; l = g_list_next (l)) {
-            GError *inner_error = NULL;
-            MMPortProbe *probe = MM_PORT_PROBE (l->data);
-            gboolean grabbed;
+            GError      *inner_error = NULL;
+            MMPortProbe *probe;
+            gboolean     grabbed = FALSE;
+            gboolean     force_ignored = FALSE;
+            const gchar *subsys;
+            const gchar *name;
+            const gchar *driver;
+            MMPortType   port_type;
+
+            probe = MM_PORT_PROBE (l->data);
+
+            subsys    = mm_port_probe_get_port_subsys (probe);
+            name      = mm_port_probe_get_port_name   (probe);
+            port_type = mm_port_probe_get_port_type   (probe);
+
+            driver    = mm_kernel_device_get_driver (mm_port_probe_peek_port (probe));
 
             /* If grabbing a port fails, just warn. We'll decide if the modem is
              * valid or not when all ports get organized */
@@ -931,45 +963,82 @@
              * probed and accepted by the generic plugin, which is overwritten
              * by the specific one when needed. */
             if (apply_subsystem_filter (self, mm_port_probe_peek_port (probe))) {
-                grabbed = FALSE;
                 inner_error = g_error_new (MM_CORE_ERROR,
                                            MM_CORE_ERROR_UNSUPPORTED,
                                            "unsupported subsystem: '%s'",
-                                           mm_port_probe_get_port_subsys (probe));
+                                           subsys);
+                goto next;
             }
+
             /* Ports that are explicitly blacklisted will be grabbed as ignored */
-            else if (mm_port_probe_is_ignored (probe)) {
-                mm_dbg ("(%s/%s): port is blacklisted",
-                        mm_port_probe_get_port_subsys (probe),
-                        mm_port_probe_get_port_name (probe));
-                grabbed = mm_base_modem_grab_port (modem,
-                                                   mm_port_probe_peek_port (probe),
-                                                   MM_PORT_TYPE_IGNORED,
-                                                   MM_PORT_SERIAL_AT_FLAG_NONE,
-                                                   &inner_error);
+            if (mm_port_probe_is_ignored (probe)) {
+                mm_dbg ("(%s/%s): port is blacklisted", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
             }
-#if !defined WITH_QMI
-            else if (mm_port_probe_get_port_type (probe) == MM_PORT_TYPE_NET &&
-                     !g_strcmp0 (mm_kernel_device_get_driver (mm_port_probe_peek_port (probe)), "qmi_wwan")) {
-                /* Try to generically grab the port, but flagged as ignored */
-                grabbed = mm_base_modem_grab_port (modem,
-                                                   mm_port_probe_peek_port (probe),
-                                                   MM_PORT_TYPE_IGNORED,
-                                                   MM_PORT_SERIAL_AT_FLAG_NONE,
-                                                   &inner_error);
+
+#if defined WITH_QMI
+            if (MM_IS_BROADBAND_MODEM_QMI (modem) &&
+                port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "qmi_wwan") != 0) {
+                /* Non-QMI net ports are ignored in QMI modems */
+                mm_dbg ("(%s/%s): ignoring non-QMI net port in QMI modem", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
+            }
+
+            if (!MM_IS_BROADBAND_MODEM_QMI (modem) &&
+                port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "qmi_wwan") == 0) {
+                /* QMI net ports are ignored in non-QMI modems */
+                mm_dbg ("(%s/%s): ignoring QMI net port in non-QMI modem", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
+            }
+#else
+            if (port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "qmi_wwan") == 0) {
+                /* QMI net ports are ignored if QMI support not built */
+                mm_dbg ("(%s/%s): ignoring QMI net port as QMI support isn't available", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
             }
 #endif
-#if !defined WITH_MBIM
-            else if (mm_port_probe_get_port_type (probe) == MM_PORT_TYPE_NET &&
-                     !g_strcmp0 (mm_kernel_device_get_driver (mm_port_probe_peek_port (probe)), "cdc_mbim")) {
-                /* Try to generically grab the port, but flagged as ignored */
+
+#if defined WITH_MBIM
+            if (MM_IS_BROADBAND_MODEM_MBIM (modem) &&
+                port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "cdc_mbim") != 0) {
+                /* Non-MBIM net ports are ignored in MBIM modems */
+                mm_dbg ("(%s/%s): ignoring non-MBIM net port in MBIM modem", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
+            }
+
+            if (!MM_IS_BROADBAND_MODEM_MBIM (modem) &&
+                port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "cdc_mbim") == 0) {
+                /* MBIM net ports are ignored in non-MBIM modems */
+                mm_dbg ("(%s/%s): ignoring MBIM net port in non-MBIM modem", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
+            }
+#else
+            if (port_type == MM_PORT_TYPE_NET &&
+                g_strcmp0 (driver, "cdc_mbim") == 0) {
+                mm_dbg ("(%s/%s): ignoring MBIM net port as MBIM support isn't available", subsys, name);
+                force_ignored = TRUE;
+                goto grab_port;
+            }
+#endif
+
+        grab_port:
+            if (force_ignored)
                 grabbed = mm_base_modem_grab_port (modem,
                                                    mm_port_probe_peek_port (probe),
                                                    MM_PORT_TYPE_IGNORED,
                                                    MM_PORT_SERIAL_AT_FLAG_NONE,
                                                    &inner_error);
-            }
-#endif
             else if (MM_PLUGIN_GET_CLASS (self)->grab_port)
                 grabbed = MM_PLUGIN_GET_CLASS (self)->grab_port (MM_PLUGIN (self),
                                                                  modem,
@@ -981,10 +1050,11 @@
                                                    mm_port_probe_get_port_type (probe),
                                                    MM_PORT_SERIAL_AT_FLAG_NONE,
                                                    &inner_error);
+
+        next:
             if (!grabbed) {
                 mm_warn ("Could not grab port (%s/%s): '%s'",
-                         mm_port_probe_get_port_subsys (MM_PORT_PROBE (l->data)),
-                         mm_port_probe_get_port_name (MM_PORT_PROBE (l->data)),
+                         subsys, name,
                          inner_error ? inner_error->message : "unknown error");
                 g_clear_error (&inner_error);
             }
diff --git a/src/mm-plugin.h b/src/mm-plugin.h
index 3b2b88b..926840b 100644
--- a/src/mm-plugin.h
+++ b/src/mm-plugin.h
@@ -106,7 +106,7 @@
 
     /* Plugins need to provide a method to create a modem object given
      * a list of port probes (Mandatory) */
-    MMBaseModem *(*create_modem) (MMPlugin *plugin,
+    MMBaseModem *(*create_modem) (MMPlugin *self,
                                   const gchar *uid,
                                   const gchar **drivers,
                                   guint16 vendor,
@@ -116,7 +116,7 @@
 
     /* Plugins need to provide a method to grab independent ports
      * identified by port probes (Optional) */
-    gboolean (*grab_port) (MMPlugin *plugin,
+    gboolean (*grab_port) (MMPlugin *self,
                            MMBaseModem *modem,
                            MMPortProbe *probe,
                            GError **error);
@@ -124,25 +124,27 @@
 
 GType mm_plugin_get_type (void);
 
-const gchar *mm_plugin_get_name (MMPlugin *plugin);
+const gchar           *mm_plugin_get_name                (MMPlugin *self);
+const gchar          **mm_plugin_get_allowed_udev_tags   (MMPlugin *self);
+const mm_uint16_pair  *mm_plugin_get_allowed_product_ids (MMPlugin *self);
 
 /* This method will run all pre-probing filters, to see if we can discard this
  * plugin from the probing logic as soon as possible. */
-MMPluginSupportsHint mm_plugin_discard_port_early (MMPlugin       *plugin,
+MMPluginSupportsHint mm_plugin_discard_port_early (MMPlugin       *self,
                                                    MMDevice       *device,
                                                    MMKernelDevice *port);
 
-void                   mm_plugin_supports_port        (MMPlugin             *plugin,
+void                   mm_plugin_supports_port        (MMPlugin             *self,
                                                        MMDevice             *device,
                                                        MMKernelDevice       *port,
                                                        GCancellable         *cancellable,
                                                        GAsyncReadyCallback   callback,
                                                        gpointer              user_data);
-MMPluginSupportsResult mm_plugin_supports_port_finish (MMPlugin             *plugin,
+MMPluginSupportsResult mm_plugin_supports_port_finish (MMPlugin             *self,
                                                        GAsyncResult         *result,
                                                        GError              **error);
 
-MMBaseModem *mm_plugin_create_modem (MMPlugin *plugin,
+MMBaseModem *mm_plugin_create_modem (MMPlugin *self,
                                      MMDevice *device,
                                      GError **error);
 
diff --git a/src/mm-port-serial-at.c b/src/mm-port-serial-at.c
index fef910e..67525af 100644
--- a/src/mm-port-serial-at.c
+++ b/src/mm-port-serial-at.c
@@ -427,6 +427,7 @@
                             buf,
                             timeout_seconds,
                             allow_cached,
+                            is_raw, /* raw commands always run next, never queued last */
                             cancellable,
                             (GAsyncReadyCallback)serial_command_ready,
                             simple);
diff --git a/src/mm-port-serial-qcdm.c b/src/mm-port-serial-qcdm.c
index 4d26e39..ec1833f 100644
--- a/src/mm-port-serial-qcdm.c
+++ b/src/mm-port-serial-qcdm.c
@@ -193,6 +193,7 @@
                             command,
                             timeout_seconds,
                             FALSE, /* never cached */
+                            FALSE, /* always queued last */
                             cancellable,
                             (GAsyncReadyCallback)serial_command_ready,
                             task);
diff --git a/src/mm-port-serial.c b/src/mm-port-serial.c
index d739313..052d0dd 100644
--- a/src/mm-port-serial.c
+++ b/src/mm-port-serial.c
@@ -163,6 +163,7 @@
                         GByteArray *command,
                         guint32 timeout_seconds,
                         gboolean allow_cached,
+                        gboolean run_next,
                         GCancellable *cancellable,
                         GAsyncReadyCallback callback,
                         gpointer user_data)
@@ -203,7 +204,12 @@
     if (!allow_cached)
         port_serial_set_cached_reply (self, ctx->command, NULL);
 
-    g_queue_push_tail (self->priv->queue, ctx);
+    /* If requested to run next, push to the head of the queue so that it really is
+     * the next one sent */
+    if (run_next)
+        g_queue_push_head (self->priv->queue, ctx);
+    else
+        g_queue_push_tail (self->priv->queue, ctx);
 
     if (g_queue_get_length (self->priv->queue) == 1)
         port_serial_schedule_queue_process (self, 0);
@@ -1381,7 +1387,10 @@
         return;
 
     if (self->priv->connected_id) {
-        g_signal_handler_disconnect (self, self->priv->connected_id);
+        /* Don't assume it's always connected, because it may be automatically connected during
+         * object disposal, and this method is also called in finalize() */
+        if (g_signal_handler_is_connected (self, self->priv->connected_id))
+            g_signal_handler_disconnect (self, self->priv->connected_id);
         self->priv->connected_id = 0;
     }
 
@@ -1587,9 +1596,18 @@
         }
     }
 
-    if (error)
+    if (error) {
+        /* An error during port reopening may mean that the device is
+         * already gone. Note that we won't get a HUP in the TTY when
+         * the port is gone during the reopen wait time, because there's
+         * no channel I/O monitoring in place.
+         *
+         * If we ever see this, we'll flag the port as forced close right
+         * away, because the open count would anyway be broken afterwards.
+         */
+        port_serial_close_force (self);
         g_task_return_error (task, error);
-    else
+    } else
         g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 
@@ -1719,23 +1737,30 @@
     return g_task_propagate_boolean (G_TASK (res), error);
 }
 
+static gboolean
+flash_cancel_cb (GTask *task)
+{
+    g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED, "Flash cancelled");
+    g_object_unref (task);
+    return G_SOURCE_REMOVE;
+}
+
 void
 mm_port_serial_flash_cancel (MMPortSerial *self)
 {
     GTask *task;
 
+    /* Do nothing if there is no flash task */
     if (!self->priv->flash_task)
         return;
 
-    /* Recover task */
+    /* Recover task and schedule it to be cancelled in an idle.
+     * We do NOT want this cancellation to happen right away,
+     * because the object reference in the flashing task may
+     * be the last one valid. */
     task = self->priv->flash_task;
     self->priv->flash_task = NULL;
-
-    g_task_return_new_error (task,
-                             MM_CORE_ERROR,
-                             MM_CORE_ERROR_CANCELLED,
-                             "Flash cancelled");
-    g_object_unref (task);
+    g_idle_add ((GSourceFunc)flash_cancel_cb, task);
 }
 
 static gboolean
diff --git a/src/mm-port-serial.h b/src/mm-port-serial.h
index bac79b4..b30bad7 100644
--- a/src/mm-port-serial.h
+++ b/src/mm-port-serial.h
@@ -144,6 +144,7 @@
                                            GByteArray *command,
                                            guint32 timeout_seconds,
                                            gboolean allow_cached,
+                                           gboolean run_next,
                                            GCancellable *cancellable,
                                            GAsyncReadyCallback callback,
                                            gpointer user_data);
diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c
index 1e0ae78..9629fab 100644
--- a/src/mm-shared-qmi.c
+++ b/src/mm-shared-qmi.c
@@ -143,6 +143,31 @@
 /*****************************************************************************/
 /* Register in network (3GPP interface) */
 
+/* wait this amount of time at most if we don't get the serving system
+ * indication earlier */
+#define REGISTER_IN_NETWORK_TIMEOUT_SECS 25
+
+typedef struct {
+    guint         timeout_id;
+    gulong        serving_system_indication_id;
+    GCancellable *cancellable;
+    gulong        cancellable_id;
+    QmiClientNas *client;
+} RegisterInNetworkContext;
+
+static void
+register_in_network_context_free (RegisterInNetworkContext *ctx)
+{
+    g_assert (!ctx->cancellable_id);
+    g_assert (!ctx->timeout_id);
+    if (ctx->client) {
+        g_assert (!ctx->serving_system_indication_id);
+        g_object_unref (ctx->client);
+    }
+    g_clear_object (&ctx->cancellable);
+    g_slice_free (RegisterInNetworkContext, ctx);
+}
+
 gboolean
 mm_shared_qmi_3gpp_register_in_network_finish (MMIfaceModem3gpp  *self,
                                                GAsyncResult      *res,
@@ -152,27 +177,150 @@
 }
 
 static void
+register_in_network_cancelled (GCancellable *cancellable,
+                               GTask        *task)
+{
+    RegisterInNetworkContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    g_assert (ctx->cancellable);
+    g_assert (ctx->cancellable_id);
+    ctx->cancellable_id = 0;
+
+    g_assert (ctx->timeout_id);
+    g_source_remove (ctx->timeout_id);
+    ctx->timeout_id = 0;
+
+    g_assert (ctx->client);
+    g_assert (ctx->serving_system_indication_id);
+    g_signal_handler_disconnect (ctx->client, ctx->serving_system_indication_id);
+    ctx->serving_system_indication_id = 0;
+
+    g_assert (g_task_return_error_if_cancelled (task));
+    g_object_unref (task);
+}
+
+static gboolean
+register_in_network_timeout (GTask *task)
+{
+    RegisterInNetworkContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+
+    g_assert (ctx->timeout_id);
+    ctx->timeout_id = 0;
+
+    g_assert (ctx->client);
+    g_assert (ctx->serving_system_indication_id);
+    g_signal_handler_disconnect (ctx->client, ctx->serving_system_indication_id);
+    ctx->serving_system_indication_id = 0;
+
+    g_assert (!ctx->cancellable || ctx->cancellable_id);
+    g_cancellable_disconnect (ctx->cancellable, ctx->cancellable_id);
+    ctx->cancellable_id = 0;
+
+    /* the 3GPP interface will take care of checking if the registration is
+     * the one we asked for */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+register_in_network_ready (GTask                               *task,
+                           QmiIndicationNasServingSystemOutput *output)
+{
+    RegisterInNetworkContext *ctx;
+    QmiNasRegistrationState   registration_state;
+
+    /* ignore indication updates reporting "searching" */
+    qmi_indication_nas_serving_system_output_get_serving_system (
+            output,
+            &registration_state,
+            NULL, /* cs_attach_state  */
+            NULL, /* ps_attach_state  */
+            NULL, /* selected_network */
+            NULL, /* radio_interfaces */
+            NULL);
+    if (registration_state == QMI_NAS_REGISTRATION_STATE_NOT_REGISTERED_SEARCHING)
+        return;
+
+    ctx = g_task_get_task_data (task);
+
+    g_assert (ctx->client);
+    g_assert (ctx->serving_system_indication_id);
+    g_signal_handler_disconnect (ctx->client, ctx->serving_system_indication_id);
+    ctx->serving_system_indication_id = 0;
+
+    g_assert (ctx->timeout_id);
+    g_source_remove (ctx->timeout_id);
+    ctx->timeout_id = 0;
+
+    g_assert (!ctx->cancellable || ctx->cancellable_id);
+    g_cancellable_disconnect (ctx->cancellable, ctx->cancellable_id);
+    ctx->cancellable_id = 0;
+
+    /* the 3GPP interface will take care of checking if the registration is
+     * the one we asked for */
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
 initiate_network_register_ready (QmiClientNas *client,
                                  GAsyncResult *res,
                                  GTask        *task)
 {
     GError                                     *error = NULL;
     QmiMessageNasInitiateNetworkRegisterOutput *output;
+    RegisterInNetworkContext                   *ctx;
+
+    ctx = g_task_get_task_data (task);
 
     output = qmi_client_nas_initiate_network_register_finish (client, res, &error);
     if (!output || !qmi_message_nas_initiate_network_register_output_get_result (output, &error)) {
-        if (!g_error_matches (error, QMI_PROTOCOL_ERROR, QMI_PROTOCOL_ERROR_NO_EFFECT)) {
+        /* No effect would mean we're already in the desired network */
+        if (g_error_matches (error, QMI_PROTOCOL_ERROR, QMI_PROTOCOL_ERROR_NO_EFFECT)) {
+            g_task_return_boolean (task, TRUE);
+            g_error_free (error);
+        } else {
             g_prefix_error (&error, "Couldn't initiate network register: ");
             g_task_return_error (task, error);
-            goto out;
         }
-        g_error_free (error);
+        g_object_unref (task);
+        goto out;
     }
 
-    g_task_return_boolean (task, TRUE);
+    /* Registration attempt started, now we need to monitor "serving system" indications
+     * to get notified when the registration changed. Note that we won't need to process
+     * the indication, because we already have that logic setup (and it runs before this
+     * new signal handler), we just need to get notified of when it happens. We will also
+     * setup a maximum operation timeuot plus a cancellability point, as this operation
+     * may be explicitly cancelled by the 3GPP interface if a new registration request
+     * arrives while the current one is being processed.
+     *
+     * Task is shared among cancellable, indication and timeout. The first one triggered
+     * will cancel the others.
+     */
+
+    if (ctx->cancellable)
+        ctx->cancellable_id = g_cancellable_connect (ctx->cancellable,
+                                                     G_CALLBACK (register_in_network_cancelled),
+                                                     task,
+                                                     NULL);
+
+    ctx->serving_system_indication_id = g_signal_connect_swapped (client,
+                                                                  "serving-system",
+                                                                  G_CALLBACK (register_in_network_ready),
+                                                                  task);
+
+    ctx->timeout_id = g_timeout_add_seconds (REGISTER_IN_NETWORK_TIMEOUT_SECS,
+                                             (GSourceFunc) register_in_network_timeout,
+                                             task);
 
 out:
-    g_object_unref (task);
 
     if (output)
         qmi_message_nas_initiate_network_register_output_unref (output);
@@ -186,9 +334,10 @@
                                         gpointer             user_data)
 {
     GTask                                     *task;
+    RegisterInNetworkContext                  *ctx;
     QmiMessageNasInitiateNetworkRegisterInput *input;
     guint16                                    mcc = 0;
-    guint16                                    mnc = 0;
+    guint16                                    mnc;
     QmiClient                                 *client = NULL;
     GError                                    *error = NULL;
 
@@ -198,7 +347,12 @@
                                       callback, user_data))
         return;
 
-    task = g_task_new (self, NULL, callback, user_data);
+    task = g_task_new (self, cancellable, callback, user_data);
+
+    ctx = g_slice_new0 (RegisterInNetworkContext);
+    ctx->client = g_object_ref (client);
+    ctx->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+    g_task_set_task_data (task, ctx, (GDestroyNotify)register_in_network_context_free);
 
     /* Parse input MCC/MNC */
     if (operator_id && !mm_3gpp_parse_operator_id (operator_id, &mcc, &mnc, &error)) {
@@ -2871,10 +3025,9 @@
     case LOAD_CARRIER_CONFIG_STEP_LAST:
         /* We will now store the loaded information so that we can later on use it
          * if needed during the automatic carrier config switching operation */
-        g_assert (!priv->config_list);
         g_assert (priv->config_active_i < 0 && !priv->config_active_default);
         g_assert (ctx->config_active_i >= 0 || ctx->config_active_default);
-        priv->config_list = g_array_ref (ctx->config_list);
+        priv->config_list = ctx->config_list ? g_array_ref (ctx->config_list) : NULL;
         priv->config_active_i = ctx->config_active_i;
         priv->config_active_default = ctx->config_active_default;
 
@@ -3975,12 +4128,13 @@
 }
 
 /*****************************************************************************/
-/* Location: internal helper: select operation mode (assisted/standalone) */
+/* Location: internal helper: select operation mode (msa/msb/standalone) */
 
 typedef enum {
     GPS_OPERATION_MODE_UNKNOWN,
     GPS_OPERATION_MODE_STANDALONE,
-    GPS_OPERATION_MODE_ASSISTED,
+    GPS_OPERATION_MODE_AGPS_MSA,
+    GPS_OPERATION_MODE_AGPS_MSB,
 } GpsOperationMode;
 
 typedef struct {
@@ -4040,7 +4194,19 @@
 
     qmi_message_pds_set_default_tracking_session_output_unref (output);
 
-    mm_dbg ("A-GPS %s", ctx->mode == GPS_OPERATION_MODE_ASSISTED ? "enabled" : "disabled");
+    switch (ctx->mode) {
+        case GPS_OPERATION_MODE_AGPS_MSA:
+            mm_dbg ("MSA A-GPS operation mode enabled");
+            break;
+        case GPS_OPERATION_MODE_AGPS_MSB:
+            mm_dbg ("MSB A-GPS operation mode enabled");
+            break;
+        case GPS_OPERATION_MODE_STANDALONE:
+            mm_dbg ("Standalone mode enabled (A-GPS disabled)");
+            break;
+        default:
+            g_assert_not_reached ();
+    }
     g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
@@ -4087,15 +4253,24 @@
 
     qmi_message_pds_get_default_tracking_session_output_unref (output);
 
-    if (ctx->mode == GPS_OPERATION_MODE_ASSISTED) {
+    if (ctx->mode == GPS_OPERATION_MODE_AGPS_MSA) {
         if (session_operation == QMI_PDS_OPERATING_MODE_MS_ASSISTED) {
-            mm_dbg ("A-GPS already enabled");
+            mm_dbg ("MSA A-GPS already enabled");
             g_task_return_boolean (task, TRUE);
             g_object_unref (task);
             return;
         }
-        mm_dbg ("Need to enable A-GPS");
+        mm_dbg ("Need to enable MSA A-GPS");
         session_operation = QMI_PDS_OPERATING_MODE_MS_ASSISTED;
+    } else if (ctx->mode == GPS_OPERATION_MODE_AGPS_MSB) {
+        if (session_operation == QMI_PDS_OPERATING_MODE_MS_BASED) {
+            mm_dbg ("MSB A-GPS already enabled");
+            g_task_return_boolean (task, TRUE);
+            g_object_unref (task);
+            return;
+        }
+        mm_dbg ("Need to enable MSB A-GPS");
+        session_operation = QMI_PDS_OPERATING_MODE_MS_BASED;
     } else if (ctx->mode == GPS_OPERATION_MODE_STANDALONE) {
         if (session_operation == QMI_PDS_OPERATING_MODE_STANDALONE) {
             mm_dbg ("A-GPS already disabled");
@@ -4164,7 +4339,19 @@
         return;
     }
 
-    mm_dbg ("A-GPS %s", ctx->mode == GPS_OPERATION_MODE_ASSISTED ? "enabled" : "disabled");
+    switch (ctx->mode) {
+        case GPS_OPERATION_MODE_AGPS_MSA:
+            mm_dbg ("MSA A-GPS operation mode enabled");
+            break;
+        case GPS_OPERATION_MODE_AGPS_MSB:
+            mm_dbg ("MSB A-GPS operation mode enabled");
+            break;
+        case GPS_OPERATION_MODE_STANDALONE:
+            mm_dbg ("Standalone mode enabled (A-GPS disabled)");
+            break;
+        default:
+            g_assert_not_reached ();
+    }
     g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
@@ -4236,15 +4423,24 @@
 
     qmi_indication_loc_get_operation_mode_output_get_operation_mode (output, &mode, NULL);
 
-    if (ctx->mode == GPS_OPERATION_MODE_ASSISTED) {
+    if (ctx->mode == GPS_OPERATION_MODE_AGPS_MSA) {
         if (mode == QMI_LOC_OPERATION_MODE_MSA) {
-            mm_dbg ("A-GPS already enabled");
+            mm_dbg ("MSA A-GPS already enabled");
             g_task_return_boolean (task, TRUE);
             g_object_unref (task);
             return;
         }
-        mm_dbg ("Need to enable A-GPS");
+        mm_dbg ("Need to enable MSA A-GPS");
         mode = QMI_LOC_OPERATION_MODE_MSA;
+    } else if (ctx->mode == GPS_OPERATION_MODE_AGPS_MSB) {
+        if (mode == QMI_LOC_OPERATION_MODE_MSB) {
+            mm_dbg ("MSB A-GPS already enabled");
+            g_task_return_boolean (task, TRUE);
+            g_object_unref (task);
+            return;
+        }
+        mm_dbg ("Need to enable MSB A-GPS");
+        mode = QMI_LOC_OPERATION_MODE_MSB;
     } else if (ctx->mode == GPS_OPERATION_MODE_STANDALONE) {
         if (mode == QMI_LOC_OPERATION_MODE_STANDALONE) {
             mm_dbg ("A-GPS already disabled");
@@ -4413,8 +4609,9 @@
                                          GAsyncResult *res,
                                          GTask        *task)
 {
-    GError  *error = NULL;
-    Private *priv;
+    MMModemLocationSource  source;
+    Private               *priv;
+    GError                *error = NULL;
 
     if (!set_gps_operation_mode_finish (self, res, &error)) {
         g_task_return_error (task, error);
@@ -4422,9 +4619,10 @@
         return;
     }
 
+    source = (MMModemLocationSource) GPOINTER_TO_UINT (g_task_get_task_data (task));
     priv = get_private (self);
 
-    priv->enabled_sources &= ~MM_MODEM_LOCATION_SOURCE_AGPS;
+    priv->enabled_sources &= ~source;
 
     g_task_return_boolean (task, TRUE);
     g_object_unref (task);
@@ -4450,8 +4648,9 @@
     /* NOTE: no parent disable_location_gathering() implementation */
 
     if (!(source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
-                    MM_MODEM_LOCATION_SOURCE_GPS_RAW |
-                    MM_MODEM_LOCATION_SOURCE_AGPS))) {
+                    MM_MODEM_LOCATION_SOURCE_GPS_RAW  |
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSB))) {
         g_task_return_boolean (task, TRUE);
         g_object_unref (task);
         return;
@@ -4460,7 +4659,7 @@
     g_assert (!(priv->pds_client && priv->loc_client));
 
     /* Disable A-GPS? */
-    if (source == MM_MODEM_LOCATION_SOURCE_AGPS) {
+    if (source == MM_MODEM_LOCATION_SOURCE_AGPS_MSA || source == MM_MODEM_LOCATION_SOURCE_AGPS_MSB) {
         set_gps_operation_mode (self,
                                 GPS_OPERATION_MODE_STANDALONE,
                                 (GAsyncReadyCallback)set_gps_operation_mode_standalone_ready,
@@ -4521,12 +4720,13 @@
 }
 
 static void
-set_gps_operation_mode_assisted_ready (MMSharedQmi  *self,
-                                       GAsyncResult *res,
-                                       GTask        *task)
+set_gps_operation_mode_agps_ready (MMSharedQmi  *self,
+                                   GAsyncResult *res,
+                                   GTask        *task)
 {
-    GError  *error = NULL;
-    Private *priv;
+    MMModemLocationSource  source;
+    Private               *priv;
+    GError                *error = NULL;
 
     if (!set_gps_operation_mode_finish (self, res, &error)) {
         g_task_return_error (task, error);
@@ -4534,9 +4734,10 @@
         return;
     }
 
+    source = (MMModemLocationSource) GPOINTER_TO_UINT (g_task_get_task_data (task));
     priv = get_private (self);
 
-    priv->enabled_sources |= MM_MODEM_LOCATION_SOURCE_AGPS;
+    priv->enabled_sources |= source;
 
     g_task_return_boolean (task, TRUE);
     g_object_unref (task);
@@ -4565,17 +4766,27 @@
     /* We only consider GPS related sources in this shared QMI implementation */
     if (!(source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
                     MM_MODEM_LOCATION_SOURCE_GPS_RAW  |
-                    MM_MODEM_LOCATION_SOURCE_AGPS))) {
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSB))) {
         g_task_return_boolean (task, TRUE);
         g_object_unref (task);
         return;
     }
 
-    /* Enabling A-GPS? */
-    if (source == MM_MODEM_LOCATION_SOURCE_AGPS) {
+    /* Enabling MSA A-GPS? */
+    if (source == MM_MODEM_LOCATION_SOURCE_AGPS_MSA) {
         set_gps_operation_mode (self,
-                                GPS_OPERATION_MODE_ASSISTED,
-                                (GAsyncReadyCallback)set_gps_operation_mode_assisted_ready,
+                                GPS_OPERATION_MODE_AGPS_MSA,
+                                (GAsyncReadyCallback)set_gps_operation_mode_agps_ready,
+                                task);
+        return;
+    }
+
+    /* Enabling MSB A-GPS? */
+    if (source == MM_MODEM_LOCATION_SOURCE_AGPS_MSB) {
+        set_gps_operation_mode (self,
+                                GPS_OPERATION_MODE_AGPS_MSB,
+                                (GAsyncReadyCallback)set_gps_operation_mode_agps_ready,
                                 task);
         return;
     }
@@ -4658,17 +4869,13 @@
 
     /* Now our own checks */
 
-    /* If we have support for the PDS client, GPS and A-GPS location is supported */
-    if (mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_PDS, MM_PORT_QMI_FLAG_DEFAULT, NULL))
+    /* If we have support for the PDS or LOC client, GPS and A-GPS location is supported */
+    if ((mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_PDS, MM_PORT_QMI_FLAG_DEFAULT, NULL)) ||
+        (mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_LOC, MM_PORT_QMI_FLAG_DEFAULT, NULL)))
         sources |= (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
                     MM_MODEM_LOCATION_SOURCE_GPS_RAW |
-                    MM_MODEM_LOCATION_SOURCE_AGPS);
-
-    /* If we have support for the LOC client, GPS location is supported */
-    if (mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_LOC, MM_PORT_QMI_FLAG_DEFAULT, NULL))
-        sources |= (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
-                    MM_MODEM_LOCATION_SOURCE_GPS_RAW |
-                    MM_MODEM_LOCATION_SOURCE_AGPS);
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSA |
+                    MM_MODEM_LOCATION_SOURCE_AGPS_MSB);
 
     /* So we're done, complete */
     g_task_return_int (task, sources);
diff --git a/src/mm-sms-list.c b/src/mm-sms-list.c
index 8d6aa15..ec3adca 100644
--- a/src/mm-sms-list.c
+++ b/src/mm-sms-list.c
@@ -278,9 +278,12 @@
     l = g_list_find_custom (self->priv->list,
                             GUINT_TO_POINTER (concat_reference),
                             (GCompareFunc)cmp_sms_by_concat_reference);
-    if (l)
+    if (l) {
         /* Try to take the part */
+        mm_dbg ("Found existing multipart SMS object with reference '%u': adding new part",
+                concat_reference);
         return mm_base_sms_multipart_take_part (MM_BASE_SMS (l->data), part, error);
+    }
 
     /* Create new Multipart */
     sms = mm_base_sms_multipart_new (self->priv->modem,
@@ -293,6 +296,9 @@
     if (!sms)
         return FALSE;
 
+    mm_dbg ("Creating new multipart SMS object: need to receive %u parts with reference '%u'",
+            mm_sms_part_get_concat_max (part),
+            concat_reference);
     self->priv->list = g_list_prepend (self->priv->list, sms);
     g_signal_emit (self, signals[SIGNAL_ADDED], 0,
                    mm_base_sms_get_path (sms),
@@ -343,15 +349,17 @@
     /* Did we just get a part of a multi-part SMS? */
     if (mm_sms_part_should_concat (part)) {
         if (mm_sms_part_get_index (part) != SMS_PART_INVALID_INDEX)
-            mm_dbg ("SMS part at '%s/%u' is from a multipart SMS (reference: '%u', sequence: '%u')",
+            mm_dbg ("SMS part at '%s/%u' is from a multipart SMS (reference: '%u', sequence: '%u/%u')",
                     mm_sms_storage_get_string (storage),
                     mm_sms_part_get_index (part),
                     mm_sms_part_get_concat_reference (part),
-                    mm_sms_part_get_concat_sequence (part));
+                    mm_sms_part_get_concat_sequence (part),
+                    mm_sms_part_get_concat_max (part));
         else
-            mm_dbg ("SMS part (not stored) is from a multipart SMS (reference: '%u', sequence: '%u')",
+            mm_dbg ("SMS part (not stored) is from a multipart SMS (reference: '%u', sequence: '%u/%u')",
                     mm_sms_part_get_concat_reference (part),
-                    mm_sms_part_get_concat_sequence (part));
+                    mm_sms_part_get_concat_sequence (part),
+                    mm_sms_part_get_concat_max (part));
 
         return take_multipart (self, part, state, storage, error);
     }
diff --git a/src/tests/test-modem-helpers.c b/src/tests/test-modem-helpers.c
index d5b09ab..18c7fdb 100644
--- a/src/tests/test-modem-helpers.c
+++ b/src/tests/test-modem-helpers.c
@@ -19,6 +19,7 @@
 #include <stdlib.h>
 #include <math.h>
 
+#define _LIBMM_INSIDE_MM
 #include <libmm-glib.h>
 #include "mm-modem-helpers.h"
 #include "mm-log.h"
@@ -2681,6 +2682,171 @@
 }
 
 /*****************************************************************************/
+/* CID selection logic */
+
+typedef struct {
+    const gchar      *apn;
+    MMBearerIpFamily  ip_family;
+    const gchar      *cgdcont_test;
+    const gchar      *cgdcont_query;
+    guint             expected_cid;
+    gboolean          expected_cid_reused;
+    gboolean          expected_cid_overwritten;
+} CidSelectionTest;
+
+static const CidSelectionTest cid_selection_tests[] = {
+    /* Test: exact APN match */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IP\",\"ac.vodafone.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 3,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 2,
+        .expected_cid_reused      = TRUE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: exact APN match reported as activated */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IP\",\"ac.vodafone.es.MNC001.MCC214.GPRS\",\"\",0,0\r\n"
+                         "+CGDCONT: 3,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 2,
+        .expected_cid_reused      = TRUE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: first empty slot in between defined contexts */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 10,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 2,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: first empty slot in between defined contexts, different PDP types */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IPV6\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 10,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 2,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: first empty slot after last context found */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 3,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: first empty slot after last context found, different PDP types */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-10),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-10),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IPV6\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 3,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = FALSE
+    },
+    /* Test: no empty slot, rewrite context with empty APN */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-3),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-3),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-3),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IP\",\"\",\"\",0,0\r\n"
+                         "+CGDCONT: 3,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 2,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = TRUE
+    },
+    /* Test: no empty slot, rewrite last context found */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = "+CGDCONT: (1-3),\"IP\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-3),\"IPV6\",,,(0,1),(0,1)\r\n"
+                         "+CGDCONT: (1-3),\"IPV4V6\",,,(0,1),(0,1)\r\n",
+        .cgdcont_query = "+CGDCONT: 1,\"IP\",\"telefonica.es\",\"\",0,0\r\n"
+                         "+CGDCONT: 2,\"IP\",\"vzwinternet\",\"\",0,0\r\n"
+                         "+CGDCONT: 3,\"IP\",\"inet.es\",\"\",0,0\r\n",
+        .expected_cid             = 3,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = TRUE
+    },
+    /* Test: CGDCONT? and CGDCONT=? failures, fallback to CID=1 (a.g. some Android phones) */
+    {
+        .apn           = "ac.vodafone.es",
+        .ip_family     = MM_BEARER_IP_FAMILY_IPV4,
+        .cgdcont_test  = NULL,
+        .cgdcont_query = NULL,
+        .expected_cid             = 1,
+        .expected_cid_reused      = FALSE,
+        .expected_cid_overwritten = TRUE
+    },
+};
+
+static void
+test_cid_selection (void)
+{
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS (cid_selection_tests); i++) {
+        const CidSelectionTest *test;
+        GList                  *context_list;
+        GList                  *context_format_list;
+        guint                   cid;
+        gboolean                cid_reused;
+        gboolean                cid_overwritten;
+
+        test = &cid_selection_tests[i];
+
+        context_format_list = test->cgdcont_test ? mm_3gpp_parse_cgdcont_test_response (test->cgdcont_test, NULL) : NULL;
+        context_list = test->cgdcont_query ? mm_3gpp_parse_cgdcont_read_response (test->cgdcont_query, NULL) : NULL;
+
+        cid = mm_3gpp_select_best_cid (test->apn, test->ip_family,
+                                       context_list, context_format_list,
+                                       &cid_reused, &cid_overwritten);
+
+        g_assert_cmpuint (cid, ==, test->expected_cid);
+        g_assert_cmpuint (cid_reused, ==, test->expected_cid_reused);
+        g_assert_cmpuint (cid_overwritten, ==, test->expected_cid_overwritten);
+
+        mm_3gpp_pdp_context_format_list_free (context_format_list);
+        mm_3gpp_pdp_context_list_free (context_list);
+    }
+}
+
+/*****************************************************************************/
 /* Test CPMS responses */
 
 static gboolean
@@ -3926,6 +4092,259 @@
 }
 
 /*****************************************************************************/
+/* +CLIP URC */
+
+typedef struct {
+    const gchar *str;
+    const gchar *number;
+    guint        type;
+} ClipUrcTest;
+
+static const ClipUrcTest clip_urc_tests[] = {
+    { "\r\n+CLIP: \"123456789\",129\r\n",      "123456789", 129 },
+    { "\r\n+CLIP: \"123456789\",129,,,,0\r\n", "123456789", 129 },
+};
+
+static void
+test_clip_indication (void)
+{
+    GRegex *r;
+    guint   i;
+
+    r = mm_voice_clip_regex_get ();
+
+    for (i = 0; i < G_N_ELEMENTS (clip_urc_tests); i++) {
+        GMatchInfo *match_info = NULL;
+        gchar      *number;
+        guint       type;
+
+        g_assert (g_regex_match (r, clip_urc_tests[i].str, 0, &match_info));
+        g_assert (g_match_info_matches (match_info));
+
+        number = mm_get_string_unquoted_from_match_info (match_info, 1);
+        g_assert_cmpstr (number, ==, clip_urc_tests[i].number);
+
+        g_assert (mm_get_uint_from_match_info (match_info, 2, &type));
+        g_assert_cmpuint (type, ==, clip_urc_tests[i].type);
+
+        g_free (number);
+        g_match_info_free (match_info);
+    }
+
+    g_regex_unref (r);
+}
+
+/*****************************************************************************/
+/* +CCWA URC */
+
+typedef struct {
+    const gchar *str;
+    const gchar *number;
+    guint        type;
+    guint        class;
+} CcwaUrcTest;
+
+static const CcwaUrcTest ccwa_urc_tests[] = {
+    { "\r\n+CCWA: \"123456789\",129,1\r\n",       "123456789", 129, 1 },
+    { "\r\n+CCWA: \"123456789\",129,1,,0\r\n",    "123456789", 129, 1 },
+    { "\r\n+CCWA: \"123456789\",129,1,,0,,,\r\n", "123456789", 129, 1 },
+};
+
+static void
+test_ccwa_indication (void)
+{
+    GRegex *r;
+    guint   i;
+
+    r = mm_voice_ccwa_regex_get ();
+
+    for (i = 0; i < G_N_ELEMENTS (ccwa_urc_tests); i++) {
+        GMatchInfo *match_info = NULL;
+        gchar      *number;
+        guint       type;
+        guint       class;
+
+        g_assert (g_regex_match (r, ccwa_urc_tests[i].str, 0, &match_info));
+        g_assert (g_match_info_matches (match_info));
+
+        number = mm_get_string_unquoted_from_match_info (match_info, 1);
+        g_assert_cmpstr (number, ==, ccwa_urc_tests[i].number);
+
+        g_assert (mm_get_uint_from_match_info (match_info, 2, &type));
+        g_assert_cmpuint (type, ==, ccwa_urc_tests[i].type);
+
+        g_assert (mm_get_uint_from_match_info (match_info, 3, &class));
+        g_assert_cmpuint (class, ==, ccwa_urc_tests[i].class);
+
+        g_free (number);
+        g_match_info_free (match_info);
+    }
+
+    g_regex_unref (r);
+}
+
+/*****************************************************************************/
+/* +CCWA service query response testing */
+
+static void
+common_test_ccwa_response (const gchar *response,
+                           gboolean     expected_status,
+                           gboolean     expected_error)
+{
+    gboolean  status = FALSE;
+    GError   *error = NULL;
+    gboolean  result;
+
+    result = mm_3gpp_parse_ccwa_service_query_response (response, &status, &error);
+
+    if (expected_error) {
+        g_assert (!result);
+        g_assert (error);
+        g_error_free (error);
+    } else {
+        g_assert (result);
+        g_assert_no_error (error);
+        g_assert_cmpuint (status, ==, expected_status);
+    }
+}
+
+typedef struct {
+    const gchar *response;
+    gboolean     expected_status;
+    gboolean     expected_error;
+} TestCcwa;
+
+static TestCcwa test_ccwa[] = {
+    { "+CCWA: 0,255", FALSE, FALSE }, /* all disabled */
+    { "+CCWA: 1,255", TRUE,  FALSE }, /* all enabled */
+    { "+CCWA: 0,1\r\n"
+      "+CCWA: 0,4\r\n", FALSE,  FALSE }, /* voice and fax disabled */
+    { "+CCWA: 1,1\r\n"
+      "+CCWA: 1,4\r\n", TRUE,  FALSE }, /* voice and fax enabled */
+    { "+CCWA: 0,2\r\n"
+      "+CCWA: 0,4\r\n"
+      "+CCWA: 0,8\r\n", FALSE,  TRUE }, /* data, fax, sms disabled, voice not given */
+    { "+CCWA: 1,2\r\n"
+      "+CCWA: 1,4\r\n"
+      "+CCWA: 1,8\r\n", FALSE,  TRUE }, /* data, fax, sms enabled, voice not given */
+    { "+CCWA: 2,1\r\n"
+      "+CCWA: 2,4\r\n", FALSE,  TRUE }, /* voice and fax enabled but unexpected state */
+};
+
+static void
+test_ccwa_response (void)
+{
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS (test_ccwa); i++)
+        common_test_ccwa_response (test_ccwa[i].response, test_ccwa[i].expected_status, test_ccwa[i].expected_error);
+}
+
+/*****************************************************************************/
+/* Test +CLCC URCs */
+
+static void
+common_test_clcc_response (const gchar      *str,
+                           const MMCallInfo *expected_call_info_list,
+                           guint             expected_call_info_list_size)
+{
+    GError     *error = NULL;
+    gboolean    result;
+    GList      *call_info_list = NULL;
+    GList      *l;
+
+    result = mm_3gpp_parse_clcc_response (str, &call_info_list, &error);
+    g_assert_no_error (error);
+    g_assert (result);
+
+    g_print ("found %u calls\n", g_list_length (call_info_list));
+
+    if (expected_call_info_list) {
+        g_assert (call_info_list);
+        g_assert_cmpuint (g_list_length (call_info_list), ==, expected_call_info_list_size);
+    } else
+        g_assert (!call_info_list);
+
+    for (l = call_info_list; l; l = g_list_next (l)) {
+        const MMCallInfo *call_info = (const MMCallInfo *)(l->data);
+        gboolean                   found = FALSE;
+        guint                      i;
+
+        g_print ("call at index %u: direction %s, state %s, number %s\n",
+                 call_info->index,
+                 mm_call_direction_get_string (call_info->direction),
+                 mm_call_state_get_string (call_info->state),
+                 call_info->number ? call_info->number : "n/a");
+
+        for (i = 0; !found && i < expected_call_info_list_size; i++)
+            found = ((call_info->index == expected_call_info_list[i].index) &&
+                     (call_info->direction  == expected_call_info_list[i].direction) &&
+                     (call_info->state  == expected_call_info_list[i].state) &&
+                     (g_strcmp0 (call_info->number, expected_call_info_list[i].number) == 0));
+
+        g_assert (found);
+    }
+
+    mm_3gpp_call_info_list_free (call_info_list);
+}
+
+static void
+test_clcc_response_empty (void)
+{
+    const gchar *response = "";
+
+    common_test_clcc_response (response, NULL, 0);
+}
+
+static void
+test_clcc_response_single (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE, "123456789" }
+    };
+
+    const gchar *response =
+        "+CLCC: 1,1,0,0,0,\"123456789\",161";
+
+    common_test_clcc_response (response, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+static void
+test_clcc_response_single_long (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_RINGING_IN, "123456789" }
+    };
+
+    /* NOTE: priority field is EMPTY */
+    const gchar *response =
+        "+CLCC: 1,1,4,0,0,\"123456789\",129,\"\",,0";
+
+    common_test_clcc_response (response, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+static void
+test_clcc_response_multiple (void)
+{
+    static const MMCallInfo expected_call_info_list[] = {
+        { 1, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  NULL        },
+        { 2, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "123456789" },
+        { 3, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "987654321" },
+        { 4, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_ACTIVE,  "000000000" },
+        { 5, MM_CALL_DIRECTION_INCOMING, MM_CALL_STATE_WAITING, "555555555" },
+    };
+
+    const gchar *response =
+        "+CLCC: 1,1,0,0,1\r\n" /* number unknown */
+        "+CLCC: 2,1,0,0,1,\"123456789\",161\r\n"
+        "+CLCC: 3,1,0,0,1,\"987654321\",161,\"Alice\"\r\n"
+        "+CLCC: 4,1,0,0,1,\"000000000\",161,\"Bob\",1\r\n"
+        "+CLCC: 5,1,5,0,0,\"555555555\",161,\"Mallory\",2,0\r\n";
+
+    common_test_clcc_response (response, expected_call_info_list, G_N_ELEMENTS (expected_call_info_list));
+}
+
+/*****************************************************************************/
 
 typedef struct {
     gchar *str;
@@ -4193,6 +4612,8 @@
     g_test_suite_add (suite, TESTCASE (test_cgdcont_read_response_nokia, NULL));
     g_test_suite_add (suite, TESTCASE (test_cgdcont_read_response_samsung, NULL));
 
+    g_test_suite_add (suite, TESTCASE (test_cid_selection, NULL));
+
     g_test_suite_add (suite, TESTCASE (test_cgact_read_response_none, NULL));
     g_test_suite_add (suite, TESTCASE (test_cgact_read_response_single_inactive, NULL));
     g_test_suite_add (suite, TESTCASE (test_cgact_read_response_single_active, NULL));
@@ -4236,6 +4657,15 @@
     g_test_suite_add (suite, TESTCASE (test_cesq_response, NULL));
     g_test_suite_add (suite, TESTCASE (test_cesq_response_to_signal, NULL));
 
+    g_test_suite_add (suite, TESTCASE (test_clip_indication, NULL));
+    g_test_suite_add (suite, TESTCASE (test_ccwa_indication, NULL));
+    g_test_suite_add (suite, TESTCASE (test_ccwa_response, NULL));
+
+    g_test_suite_add (suite, TESTCASE (test_clcc_response_empty, NULL));
+    g_test_suite_add (suite, TESTCASE (test_clcc_response_single, NULL));
+    g_test_suite_add (suite, TESTCASE (test_clcc_response_single_long, NULL));
+    g_test_suite_add (suite, TESTCASE (test_clcc_response_multiple, NULL));
+
     g_test_suite_add (suite, TESTCASE (test_parse_uint_list, NULL));
 
     g_test_suite_add (suite, TESTCASE (test_bcd_to_string, NULL));
diff --git a/test/Makefile.am b/test/Makefile.am
index cd2b5d5..20e2a06 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -67,6 +67,56 @@
 	$(NULL)
 
 ################################################################################
+# mmsmspdu
+################################################################################
+
+noinst_PROGRAMS += mmsmspdu
+
+mmsmspdu_SOURCES = mmsmspdu.c
+
+mmsmspdu_CPPFLAGS = \
+	$(MM_CFLAGS) \
+	-I$(top_srcdir) \
+	-I$(top_srcdir)/src \
+	-I$(top_srcdir)/include \
+	-I$(top_builddir)/include \
+	-I$(top_srcdir)/libmm-glib \
+	-I$(top_srcdir)/libmm-glib/generated \
+	-I$(top_builddir)/libmm-glib/generated
+	$(NULL)
+
+mmsmspdu_LDADD = \
+	$(MM_LIBS) \
+	$(top_builddir)/libmm-glib/libmm-glib.la \
+	$(top_builddir)/src/libhelpers.la \
+	$(NULL)
+
+################################################################################
+# mmsmsmonitor
+################################################################################
+
+noinst_PROGRAMS += mmsmsmonitor
+
+mmsmsmonitor_SOURCES = mmsmsmonitor.c
+
+mmsmsmonitor_CPPFLAGS = \
+	$(MM_CFLAGS) \
+	-I$(top_srcdir) \
+	-I$(top_srcdir)/src \
+	-I$(top_srcdir)/include \
+	-I$(top_builddir)/include \
+	-I$(top_srcdir)/libmm-glib \
+	-I$(top_srcdir)/libmm-glib/generated \
+	-I$(top_builddir)/libmm-glib/generated
+	$(NULL)
+
+mmsmsmonitor_LDADD = \
+	$(MM_LIBS) \
+	$(top_builddir)/libmm-glib/libmm-glib.la \
+	$(top_builddir)/src/libhelpers.la \
+	$(NULL)
+
+################################################################################
 # mmcli-test-sms
 ################################################################################
 
diff --git a/test/lsudev.c b/test/lsudev.c
index 7b8860e..fe5d44c 100644
--- a/test/lsudev.c
+++ b/test/lsudev.c
@@ -30,94 +30,94 @@
 static void
 signal_handler (int signo)
 {
-	if (signo == SIGINT || signo == SIGTERM) {
-		g_message ("Caught signal %d, shutting down...", signo);
-		g_main_loop_quit (loop);
-	}
+    if (signo == SIGINT || signo == SIGTERM) {
+        g_message ("Caught signal %d, shutting down...", signo);
+        g_main_loop_quit (loop);
+    }
 }
 
 static void
 setup_signals (void)
 {
-	struct sigaction action;
-	sigset_t mask;
+    struct sigaction action;
+    sigset_t mask;
 
-	sigemptyset (&mask);
-	action.sa_handler = signal_handler;
-	action.sa_mask = mask;
-	action.sa_flags = 0;
-	sigaction (SIGTERM,  &action, NULL);
-	sigaction (SIGINT,  &action, NULL);
+    sigemptyset (&mask);
+    action.sa_handler = signal_handler;
+    action.sa_mask = mask;
+    action.sa_flags = 0;
+    sigaction (SIGTERM,  &action, NULL);
+    sigaction (SIGINT,  &action, NULL);
 }
 
 static void
 println (guint indent, const char *fmt, ...)
 {
-	va_list args;
-	GString *output;
-	int i;
+    va_list args;
+    GString *output;
+    int i;
 
-	g_return_if_fail (fmt != NULL);
+    g_return_if_fail (fmt != NULL);
 
-	output = g_string_sized_new (250);
+    output = g_string_sized_new (250);
 
-	for (i = 0; i < indent; i++)
-		g_string_append_c (output, ' ');
+    for (i = 0; i < indent; i++)
+        g_string_append_c (output, ' ');
 
-	va_start (args, fmt);
-	g_string_append_vprintf (output, fmt, args);
-	va_end (args);
+    va_start (args, fmt);
+    g_string_append_vprintf (output, fmt, args);
+    va_end (args);
 
-	g_print ("%s\n", output->str);
-	g_string_free (output, TRUE);
+    g_print ("%s\n", output->str);
+    g_string_free (output, TRUE);
 }
 
 static void
 dump_device_and_parent (GUdevDevice *device, guint indent)
 {
-	const char **list, **iter;
-	GUdevDevice *parent;
-	char propstr[500];
-	guint32 namelen = 0, i;
+    const char **list, **iter;
+    GUdevDevice *parent;
+    char propstr[500];
+    guint32 namelen = 0, i;
 
-	println (indent, "------------------------------------------------------");
-	println (indent, "Name:     %s", g_udev_device_get_name (device));
-	println (indent, "Type:     %s", g_udev_device_get_devtype (device));
-	println (indent, "Subsys:   %s", g_udev_device_get_subsystem (device));
-	println (indent, "Number:   %s", g_udev_device_get_number (device));
-	println (indent, "Path:     %s", g_udev_device_get_sysfs_path (device));
-	println (indent, "Driver:   %s", g_udev_device_get_driver (device));
-	println (indent, "Action:   %s", g_udev_device_get_action (device));
-	println (indent, "Seq Num:  %lu", g_udev_device_get_seqnum (device));
-	println (indent, "Dev File: %s", g_udev_device_get_device_file (device));
+    println (indent, "------------------------------------------------------");
+    println (indent, "Name:     %s", g_udev_device_get_name (device));
+    println (indent, "Type:     %s", g_udev_device_get_devtype (device));
+    println (indent, "Subsys:   %s", g_udev_device_get_subsystem (device));
+    println (indent, "Number:   %s", g_udev_device_get_number (device));
+    println (indent, "Path:     %s", g_udev_device_get_sysfs_path (device));
+    println (indent, "Driver:   %s", g_udev_device_get_driver (device));
+    println (indent, "Action:   %s", g_udev_device_get_action (device));
+    println (indent, "Seq Num:  %lu", g_udev_device_get_seqnum (device));
+    println (indent, "Dev File: %s", g_udev_device_get_device_file (device));
 
-	println (indent, "");
-	println (indent, "Properties:");
+    println (indent, "");
+    println (indent, "Properties:");
 
-	/* Get longest property name length for alignment */
-	list = (const char **) g_udev_device_get_property_keys (device);
-	for (iter = list; iter && *iter; iter++) {
-		if (strlen (*iter) > namelen)
-			namelen = strlen (*iter);
-	}
-	namelen++;
+    /* Get longest property name length for alignment */
+    list = (const char **) g_udev_device_get_property_keys (device);
+    for (iter = list; iter && *iter; iter++) {
+        if (strlen (*iter) > namelen)
+            namelen = strlen (*iter);
+    }
+    namelen++;
 
-	for (iter = list; iter && *iter; iter++) {
-		strcpy (propstr, *iter);
-		strcat (propstr, ":");
-		for (i = 0; i < namelen - strlen (*iter); i++)
-			strcat (propstr, " ");
-		strcat (propstr, g_udev_device_get_property (device, *iter));
-		println (indent + 2, "%s", propstr);
-	}
+    for (iter = list; iter && *iter; iter++) {
+        strcpy (propstr, *iter);
+        strcat (propstr, ":");
+        for (i = 0; i < namelen - strlen (*iter); i++)
+            strcat (propstr, " ");
+        strcat (propstr, g_udev_device_get_property (device, *iter));
+        println (indent + 2, "%s", propstr);
+    }
 
-	println (indent, "");
+    println (indent, "");
 
-	parent = g_udev_device_get_parent (device);
-	if (parent) {
-		dump_device_and_parent (parent, indent + 4);
-		g_object_unref (parent);
-	}
+    parent = g_udev_device_get_parent (device);
+    if (parent) {
+        dump_device_and_parent (parent, indent + 4);
+        g_object_unref (parent);
+    }
 }
 
 static void
@@ -126,53 +126,53 @@
                GUdevDevice *device,
                gpointer user_data)
 {
-	const char *expected_subsys = user_data;
-	const char *subsys;
+    const char *expected_subsys = user_data;
+    const char *subsys;
 
-	g_return_if_fail (client != NULL);
-	g_return_if_fail (action != NULL);
-	g_return_if_fail (device != NULL);
+    g_return_if_fail (client != NULL);
+    g_return_if_fail (action != NULL);
+    g_return_if_fail (device != NULL);
 
-	/* A bit paranoid */
-	subsys = g_udev_device_get_subsystem (device);
-	g_return_if_fail (subsys != NULL);
+    /* A bit paranoid */
+    subsys = g_udev_device_get_subsystem (device);
+    g_return_if_fail (subsys != NULL);
 
-	g_return_if_fail (!strcmp (subsys, expected_subsys));
+    g_return_if_fail (!strcmp (subsys, expected_subsys));
 
-	g_print ("---- (EVENT: %s) ----\n", action);
-	dump_device_and_parent (device, 0);
-	g_print ("\n");
+    g_print ("---- (EVENT: %s) ----\n", action);
+    dump_device_and_parent (device, 0);
+    g_print ("\n");
 }
 
 int
 main (int argc, char *argv[])
 {
-	GUdevClient *client;	
-	const char *subsys[2] = { NULL, NULL };
-	GList *list, *iter;
+    GUdevClient *client;
+    const char *subsys[2] = { NULL, NULL };
+    GList *list, *iter;
 
-	if (argc != 2) {
-		g_warning ("Usage: %s [subsystem]", argv[0]);
-		return 1;
-	}
+    if (argc != 2) {
+        g_warning ("Usage: %s [subsystem]", argv[0]);
+        return 1;
+    }
 
-	loop = g_main_loop_new (NULL, FALSE);
+    loop = g_main_loop_new (NULL, FALSE);
 
-	setup_signals ();
+    setup_signals ();
 
-	subsys[0] = argv[1];
-	client = g_udev_client_new (subsys);
-	g_signal_connect (client, "uevent", G_CALLBACK (handle_uevent), (gpointer) subsys[0]);
+    subsys[0] = argv[1];
+    client = g_udev_client_new (subsys);
+    g_signal_connect (client, "uevent", G_CALLBACK (handle_uevent), (gpointer) subsys[0]);
 
-	list = g_udev_client_query_by_subsystem (client, subsys[0]);
-	for (iter = list; iter; iter = g_list_next (iter)) {
-		dump_device_and_parent (G_UDEV_DEVICE (iter->data), 0);
-		g_print ("\n");
-		g_object_unref (G_UDEV_DEVICE (iter->data));
-	}
+    list = g_udev_client_query_by_subsystem (client, subsys[0]);
+    for (iter = list; iter; iter = g_list_next (iter)) {
+        dump_device_and_parent (G_UDEV_DEVICE (iter->data), 0);
+        g_print ("\n");
+        g_object_unref (G_UDEV_DEVICE (iter->data));
+    }
 
-	g_main_loop_run (loop);
+    g_main_loop_run (loop);
 
-	return 0;
+    return 0;
 }
 
diff --git a/test/mmsmsmonitor.c b/test/mmsmsmonitor.c
new file mode 100644
index 0000000..de89521
--- /dev/null
+++ b/test/mmsmsmonitor.c
@@ -0,0 +1,195 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+
+#include <glib.h>
+#include <glib-unix.h>
+#include <gio/gio.h>
+#include <libmm-glib.h>
+
+#define PROGRAM_NAME    "mmsmsmonitor"
+#define PROGRAM_VERSION PACKAGE_VERSION
+
+/* Globals */
+static GMainLoop *loop;
+static GList     *monitored_sms_list;
+
+/* Context */
+static gboolean version_flag;
+
+static GOptionEntry main_entries[] = {
+    { "version", 'V', 0, G_OPTION_ARG_NONE, &version_flag,
+      "Print version",
+      NULL
+    },
+    { NULL }
+};
+
+static gboolean
+signals_handler (void)
+{
+    if (loop && g_main_loop_is_running (loop)) {
+        g_printerr ("%s\n",
+                    "cancelling the main loop...\n");
+        g_main_loop_quit (loop);
+    }
+    return TRUE;
+}
+
+static void
+print_version_and_exit (void)
+{
+    g_print ("\n"
+             PROGRAM_NAME " " PROGRAM_VERSION "\n"
+             "Copyright (2019) Aleksander Morgado\n"
+             "License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl-2.0.html>\n"
+             "This is free software: you are free to change and redistribute it.\n"
+             "There is NO WARRANTY, to the extent permitted by law.\n"
+             "\n");
+    exit (EXIT_SUCCESS);
+}
+
+static void
+sms_state_updated (MMSms *sms)
+{
+    g_print ("[%s] sms updated: %s\n",
+             mm_sms_get_path (sms),
+             mm_sms_state_get_string (mm_sms_get_state (sms)));
+}
+
+static gboolean
+sms_added (MMModemMessaging *modem_messaging,
+           const gchar      *sms_path,
+           gboolean          received)
+{
+    GList *sms_list;
+    GList *l;
+    MMSms *new_sms = NULL;
+
+    sms_list = mm_modem_messaging_list_sync (modem_messaging, NULL, NULL);
+    for (l = sms_list; l && !new_sms; l = g_list_next (l)) {
+        MMSms *l_sms = MM_SMS  (l->data);
+
+        if (g_strcmp0 (mm_sms_get_path (l_sms), sms_path) == 0)
+            new_sms = l_sms;
+    }
+    g_assert (new_sms);
+
+    g_print ("[%s] new sms: %s\n",
+             mm_sms_get_path (new_sms),
+             mm_sms_state_get_string (mm_sms_get_state (new_sms)));
+    g_signal_connect (new_sms, "notify::state", G_CALLBACK (sms_state_updated), NULL);
+    monitored_sms_list = g_list_append (monitored_sms_list, g_object_ref (new_sms));
+
+    g_list_free_full (sms_list, g_object_unref);
+    return TRUE;
+}
+
+static void
+list_all_sms_found (MMModemMessaging *modem_messaging)
+{
+    GList *sms_list;
+    GList *l;
+
+    sms_list = mm_modem_messaging_list_sync (modem_messaging, NULL, NULL);
+    for (l = sms_list; l; l = g_list_next (l)) {
+        MMSms *l_sms = MM_SMS  (l->data);
+
+        g_print ("[%s] sms found: %s\n",
+                 mm_sms_get_path (l_sms),
+                 mm_sms_state_get_string (mm_sms_get_state (l_sms)));
+        g_signal_connect (l_sms, "notify::state", G_CALLBACK (sms_state_updated), NULL);
+        monitored_sms_list = g_list_append (monitored_sms_list, g_object_ref (l_sms));
+    }
+    g_list_free_full (sms_list, g_object_unref);
+}
+
+int main (int argc, char **argv)
+{
+    GOptionContext  *context;
+    GDBusConnection *connection;
+    MMManager       *manager;
+    GList           *modem_list;
+    GList           *l;
+    gchar           *name_owner;
+    GError          *error = NULL;
+
+    setlocale (LC_ALL, "");
+
+    /* Setup option context, process it and destroy it */
+    context = g_option_context_new ("- ModemManager SMS monitor");
+    g_option_context_add_main_entries (context, main_entries, NULL);
+    g_option_context_parse (context, &argc, &argv, NULL);
+    g_option_context_free (context);
+
+    if (version_flag)
+        print_version_and_exit ();
+
+    /* Setup dbus connection to use */
+    connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
+    if (!connection) {
+        g_printerr ("error: couldn't get bus: %s\n",
+                    error ? error->message : "unknown error");
+        exit (EXIT_FAILURE);
+    }
+
+    manager = mm_manager_new_sync (connection,
+                                   G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
+                                   NULL,
+                                   &error);
+    if (!manager) {
+        g_printerr ("error: couldn't create manager: %s\n",
+                    error ? error->message : "unknown error");
+        exit (EXIT_FAILURE);
+    }
+
+    name_owner = g_dbus_object_manager_client_get_name_owner (G_DBUS_OBJECT_MANAGER_CLIENT (manager));
+    if (!name_owner) {
+        g_printerr ("error: couldn't find the ModemManager process in the bus\n");
+        exit (EXIT_FAILURE);
+    }
+    g_free (name_owner);
+
+    modem_list = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (manager));
+    for (l = modem_list; l; l = g_list_next (l)) {
+        MMObject         *obj;
+        MMModemMessaging *modem_messaging;
+
+        obj             = MM_OBJECT (l->data);
+        modem_messaging = MM_MODEM_MESSAGING (mm_object_peek_modem_messaging (obj));
+
+        g_signal_connect (modem_messaging, "added", G_CALLBACK (sms_added), NULL);
+        list_all_sms_found (modem_messaging);
+    }
+
+    g_unix_signal_add (SIGINT,  (GSourceFunc) signals_handler, NULL);
+    g_unix_signal_add (SIGHUP,  (GSourceFunc) signals_handler, NULL);
+    g_unix_signal_add (SIGTERM, (GSourceFunc) signals_handler, NULL);
+
+    /* Setup main loop and shedule start in idle */
+    loop = g_main_loop_new (NULL, FALSE);
+    g_main_loop_run (loop);
+
+    /* Cleanup */
+    g_main_loop_unref (loop);
+    g_list_free_full (modem_list, g_object_unref);
+    g_object_unref (manager);
+    return EXIT_SUCCESS;
+}
diff --git a/test/mmsmspdu.c b/test/mmsmspdu.c
new file mode 100644
index 0000000..cb56c71
--- /dev/null
+++ b/test/mmsmspdu.c
@@ -0,0 +1,233 @@
+/* -*- 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) 2019 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <string.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include <ModemManager.h>
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+#include "mm-log.h"
+#include "mm-sms-part-3gpp.h"
+
+#define PROGRAM_NAME    "mmsmspdu"
+#define PROGRAM_VERSION PACKAGE_VERSION
+
+/* Context */
+static gchar    *pdu;
+static gboolean  verbose_flag;
+static gboolean  version_flag;
+
+static GOptionEntry main_entries[] = {
+    { "pdu", 'p', 0, G_OPTION_ARG_STRING, &pdu,
+      "PDU contents",
+      "[0123456789ABCDEF..]"
+    },
+    { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose_flag,
+      "Run action with verbose logs",
+      NULL
+    },
+    { "version", 'V', 0, G_OPTION_ARG_NONE, &version_flag,
+      "Print version",
+      NULL
+    },
+    { NULL }
+};
+
+static void
+show_part_info (MMSmsPart *part)
+{
+    MMSmsPduType   pdu_type;
+    const gchar   *smsc;
+    const gchar   *number;
+    const gchar   *timestamp;
+    const gchar   *text;
+    MMSmsEncoding  encoding;
+    gint           class;
+    guint          validity_relative;
+    gboolean       delivery_report_request;
+    guint          concat_reference;
+    guint          concat_max;
+    guint          concat_sequence;
+    const GByteArray *data;
+
+    pdu_type = mm_sms_part_get_pdu_type (part);
+    g_print ("pdu type: %s\n", mm_sms_pdu_type_get_string (pdu_type));
+
+    smsc = mm_sms_part_get_smsc (part);
+    g_print ("smsc: %s\n", smsc ? smsc : "n/a");
+
+    number = mm_sms_part_get_number (part);
+    g_print ("number: %s\n", number ? number : "n/a");
+
+    timestamp = mm_sms_part_get_timestamp (part);
+    g_print ("timestamp: %s\n", timestamp ? timestamp : "n/a");
+
+    encoding = mm_sms_part_get_encoding (part);
+    switch (encoding) {
+    case MM_SMS_ENCODING_GSM7:
+        g_print ("encoding: GSM7\n");
+        break;
+    case MM_SMS_ENCODING_UCS2:
+        g_print ("encoding: UCS2\n");
+        break;
+    case MM_SMS_ENCODING_8BIT:
+        g_print ("encoding: 8BIT\n");
+        break;
+    default:
+        g_print ("encoding: unknown (0x%x)\n", encoding);
+        break;
+    }
+
+    text = mm_sms_part_get_text (part);
+    g_print ("text: %s\n", text ? text : "n/a");
+
+    data = mm_sms_part_get_data (part);
+    if (data) {
+      gchar *data_str;
+
+      data_str = mm_utils_bin2hexstr (data->data, data->len);
+      g_print ("data: %s\n", data_str);
+      g_free (data_str);
+    } else
+      g_print ("data: n/a\n");
+
+    class = mm_sms_part_get_class (part);
+    if (class != -1)
+        g_print ("class: %d\n", class);
+    else
+        g_print ("class: n/a\n");
+
+    validity_relative = mm_sms_part_get_validity_relative (part);
+    if (validity_relative != 0)
+        g_print ("validity relative: %d\n", validity_relative);
+    else
+        g_print ("validity relative: n/a\n");
+
+    delivery_report_request = mm_sms_part_get_delivery_report_request (part);
+    g_print ("delivery report request: %s\n", delivery_report_request ? "yes" : "no");
+
+    concat_reference = mm_sms_part_get_concat_reference (part);
+    g_print ("concat reference: %d\n", concat_reference);
+
+    concat_max = mm_sms_part_get_concat_max (part);
+    g_print ("concat max: %d\n", concat_max);
+
+    concat_sequence = mm_sms_part_get_concat_sequence (part);
+    g_print ("concat sequence: %d\n", concat_sequence);
+
+    if (mm_sms_part_get_pdu_type (part) == MM_SMS_PDU_TYPE_STATUS_REPORT) {
+       const gchar *discharge_timestamp;
+       guint        message_reference;
+       guint        delivery_state;
+
+       message_reference = mm_sms_part_get_message_reference (part);
+       g_print ("message reference: %d\n", message_reference);
+
+       discharge_timestamp = mm_sms_part_get_discharge_timestamp (part);
+       g_print ("discharge timestamp: %s\n", discharge_timestamp ? discharge_timestamp : "n/a");
+
+       delivery_state = mm_sms_part_get_delivery_state (part);
+       g_print ("delivery state: %s\n", mm_sms_delivery_state_get_string_extended (delivery_state));
+    }
+
+    if (MM_SMS_PART_IS_CDMA (part)) {
+        MMSmsCdmaTeleserviceId   teleservice_id;
+        MMSmsCdmaServiceCategory service_category;
+
+        teleservice_id = mm_sms_part_get_cdma_teleservice_id (part);
+        g_print ("teleservice id: %s\n", mm_sms_cdma_teleservice_id_get_string (teleservice_id));
+
+        service_category = mm_sms_part_get_cdma_service_category (part);
+        g_print ("service category: %s\n", mm_sms_cdma_service_category_get_string (service_category));
+    }
+}
+
+void
+_mm_log (const char *loc,
+         const char *func,
+         guint32     level,
+         const char *fmt,
+         ...)
+{
+    va_list args;
+    gchar *msg;
+
+    if (!verbose_flag)
+        return;
+
+    va_start (args, fmt);
+    msg = g_strdup_vprintf (fmt, args);
+    va_end (args);
+    g_print ("%s\n", msg);
+    g_free (msg);
+}
+
+static void
+print_version_and_exit (void)
+{
+    g_print ("\n"
+             PROGRAM_NAME " " PROGRAM_VERSION "\n"
+             "Copyright (2019) Aleksander Morgado\n"
+             "License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl-2.0.html>\n"
+             "This is free software: you are free to change and redistribute it.\n"
+             "There is NO WARRANTY, to the extent permitted by law.\n"
+             "\n");
+    exit (EXIT_SUCCESS);
+}
+
+int main (int argc, char **argv)
+{
+    GOptionContext *context;
+    GError         *error = NULL;
+    MMSmsPart      *part;
+
+    setlocale (LC_ALL, "");
+
+    /* Setup option context, process it and destroy it */
+    context = g_option_context_new ("- ModemManager SMS PDU parser");
+    g_option_context_add_main_entries (context, main_entries, NULL);
+    g_option_context_parse (context, &argc, &argv, NULL);
+    g_option_context_free (context);
+
+    if (version_flag)
+        print_version_and_exit ();
+
+    /* No pdu given? */
+    if (!pdu) {
+        g_printerr ("error: no PDU specified\n");
+        exit (EXIT_FAILURE);
+    }
+
+    part = mm_sms_part_3gpp_new_from_pdu (0, pdu, &error);
+    if (!part) {
+        g_printerr ("error: couldn't parse PDU: %s\n", error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    show_part_info (part);
+
+    mm_sms_part_free (part);
+    g_free (pdu);
+
+    return EXIT_SUCCESS;
+}