Merge cros/upstream to cros/master

Contains the following commits:
 8bc90b71 broadband-modem-qmi: Enable AT URCs and QMI indications (Dylan Van Assche)
 438ff54d libmm-glib,tests: include string.h explicitly (Aleksander Morgado)
 da0e610f modem-helpers-mbim: include string.h explicitly (Aleksander Morgado)
 a9611c62 utils: import ptr array lookup with GEqualFunc from GLib 2.54 (Aleksander Morgado)
 e1372a71 iface-modem: detect hotswap on all slots (Pavan Holla)
 fbc16360 docs: add api index for 1.18 (Aleksander Morgado)
 a4aba0a6 mmcli,sim: add preferred networks list to SIM properties (Teijo Kinnunen)
 816beeff libmm-glib,modem-helpers,mm-base-sim: implement Sim.PreferredNetworks (Teijo Kinnunen)
 c7d36667 shared-qmi: network registration cancellation logic with asserts disabled (Aleksander Morgado)
 f10e4af9 libmm-glib,bearer-properties: fix 'allow roaming' comparison (Aleksander Morgado)
 297a8c85 examples: sms: resolve PEP8 issues (Yegor Yefremov)
 aba237df broadband-modem-qmi: allow lookup of QMI for data without SIO port (Aleksander Morgado)
 381e2f38 base-modem: separate method to lookup exact port by name (Aleksander Morgado)
 b8e076f9 kernel-device-udev: keep track of the client object (Aleksander Morgado)
 1b35d74c kernel-device: add get_interface_number() method (Aleksander Morgado)
 cc07d214 examples: network-scan: get rid of global variables (Yegor Yefremov)
 62506034 build: improve releasing notes (Aleksander Morgado)
 4a06a027 charsets: detect iconv() support in runtime (Aleksander Morgado)
 8a8e0016 charsets: define common translit fallback character (Aleksander Morgado)
 c84454c1 charsets: remove charset_hex_to_utf8() (Aleksander Morgado)
 0ff3eb7e charsets: remove take_and_convert methods (Aleksander Morgado)
 ab4c31ec cinterion: rework mno decoding to use str_to_utf8() (Aleksander Morgado)
 6bc07b4b cinterion: rework band encoding to use str_to_utf8() (Aleksander Morgado)
 16df1e17 helpers: rework normalize_operator() to use str_to_utf8() (Aleksander Morgado)
 63fa9eee charsets,tests: update take_and_convert tests to str_from/to (Aleksander Morgado)
 3ac248a7 cinterion: move sequence to set bands to private ctx (Aleksander Morgado)
 e5363b54 charsets: use new str_from_utf8() instead of take_and_convert_to_current_charset() (Aleksander Morgado)
 395ab06c charsets: use new bytearray_to_utf8() instead of hex_to_utf8() (Aleksander Morgado)
 5ea4a591 charsets: use new bytearray_to_utf8() instead of byte_array_to_utf8() (Aleksander Morgado)
 033e174e charsets: make charset_gsm_unpacked_to_utf8() private (Aleksander Morgado)
 8bfdfb18 charsets: use new bytearray_from_utf8() instead of byte_array_append() (Aleksander Morgado)
 75b37e16 charsets: make charset_utf8_to_unpacked_gsm() private (Aleksander Morgado)
 9c613d33 charsets: new common APIs to convert from/to charsets and UTF-8 (Aleksander Morgado)
 6f32c8d3 charsets: avoid //TRANSLIT when converting to/from charsets (Aleksander Morgado)
 bc449cbe charsets: make translit optional in utf8_to_unpacked_gsm() (Aleksander Morgado)
 5ce97abd charsets: make translit optional in gsm_unpacked_to_utf8() (Aleksander Morgado)
 5480cb67 libmm-glib,tests: add ishexstr/hexstr2bin/bin2hexstr unit tests (Aleksander Morgado)
 34de613d libmm-glib,common-helpers: make hexstr2bin() return a guint8 array (Aleksander Morgado)
 6d8610d6 libmm-glib,common-helpers: ishexstr() fails on empty input string (Aleksander Morgado)
 8c30a6b6 libmm-glib,common-helpers: hexstr2bin fails on empty input string (Aleksander Morgado)
 a211981d libmm-glib,common-helpers: make hexstr2bin() accept input string length (Aleksander Morgado)
 657cabcf libmm-glib,common-helpers: make hexstr2bin() return a GError (Aleksander Morgado)
 dbdf67e9 charsets: remove unused charset_utf8_to_hex() method (Aleksander Morgado)
 8b590721 charsets: don't allow quoting in byte_array_append() (Aleksander Morgado)
 38a4a9c8 charsets: remove HEX charset type (Aleksander Morgado)
 a025e83e charsets: define charset enum explicitly as flags (Aleksander Morgado)
 19e5d5f9 build: post-release version bump to 1.17.0 (Aleksander Morgado)
 7a5a49b7 release: bump version to 1.16.0 (Aleksander Morgado)
 7a5eae2a NEWS: update for 1.16.0 (Aleksander Morgado)
 bbd3638d build: require libqmi 1.28.0 (Aleksander Morgado)
 a5462014 bearer-mbim: IP type may be reported as deactivated and still have IP settings (Aleksander Morgado)

Cq-Depend: chromium:2729495
Change-Id: Ib418fd49adc23055e82037d69469da794442425e
diff --git a/NEWS b/NEWS
index a57854b..2ded561 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,149 @@
 
+ModemManager 1.16.0
+-------------------------------------------
+This is a new stable release of ModemManager.
+
+The following notes are directed to package maintainers:
+
+ * This version now requires:
+   ** libqmi >= 1.28.0 (for the optional QMI support)
+
+ * The 1.16.x branch will be the last one supporting the 'LEGACY' and 'PARANOID'
+   filter modes; standard distributions are advised to use the default 'STRICT'
+   mode if they aren't using it already (i.e. running the daemon without any
+   explicit '--filter-policy' option).
+
+ * A new 'qcom-soc' plugin is implemented to be able to use ModemManager in
+   Qualcomm SoCs like the MSM8916 or MSM8974. This plugin uses a combination of
+   RPMSG based control ports plus BAM-DMUX based network ports. This plugin is
+   disabled by default, even when `--enable-all-plugins` is used, and if wanted
+   it must be explicitly enabled with `--enable-plugin-qcom-soc`. Systems
+   targeting this kind of SoCs, like postmarketos, should enable it. Standard
+   distributions may or may not include it, up to the targeted hardware in each
+   distribution.
+
+ * Gentoo's 'libelogind' library may now be used to detect the systemd
+   suspend/resume support.
+
+The API is backwards compatible with the previous releases, the only updates
+are the following:
+
+ * Modem interface:
+   ** Updated the 'Ports' property so that it exposes all ports that are
+      owned by the modem even if they are explicitly ignored and not used.
+   ** New 'SimSlots' property that exposes the available SIM slots in the modem,
+      including the SIM object paths in each of them if the cards are present.
+   ** New 'PrimarySimSlot' property indicating which of the slots in the
+      'SimSlots' array is the one currently active.
+   ** New 'SetPrimarySimSlot' method to select which SIM slot in the 'SimSlots'
+      array should be considered active. When the switch happens, the modem will
+      be fully re-probed.
+
+ * Signal interface:
+   ** New 'Nr5g' dictionary property including signal information for the 5GNR
+      access technology.
+
+ * SIM interface:
+   ** New 'Active' boolean property, indicating whether the SIM object is the
+      currently active one.
+   ** New 'Eid' string property, indicating the EID of the card, if any.
+
+ * New udev tags:
+   ** New 'ID_MM_PORT_TYPE_QMI' tag to explicitly flag a port as being QMI, when
+      there is no other way to guess the type of port; e.g. this tag is not
+      needed for ports exposed by the qmi_wwan driver.
+   ** New 'ID_MM_PORT_TYPE_MBIM' tag to explicitly flag a port as being MBIM,
+      when there is no other way to guess the type of port; e.g. this tag is not
+      needed for ports exposed by the cdc_mbim driver.
+
+The most important features and changes in this release are the following:
+
+ * Implemented support for Multi SIM Single Standby support, for systems that
+   have multiple SIM slots and they can select which of them (only one) is
+   active at any given time. Currently implemented for QMI modems only.
+
+ * If the modem enabling phase fails in a fatal way, an implicit disabling
+   sequence is now run, in order to avoid leaving the modem in an inconsistent
+   state.
+
+ * If the connection attempt includes user/password information but no explicit
+   authentication type, CHAP will now be used by default instead of PAP.
+
+ * Full USB device removal events reported via udev are no longer used. The
+   device removal logic relies exclusively on independent port removal events,
+   as that logic is supported for all subsystems and kernel device backends
+   (e.g. also working for non-USB devices and for systems without udev like
+   OpenWRT).
+
+ * Added support to monitor the 'rpmsg' subsystem, but only in plugins that
+   explicitly require its use (e.g. the 'qcom-soc' plugin).
+
+ * New options in the ModemManager daemon:
+   ** Added new '--test-no-suspend-resume' option to disable the runtime
+      suspend/resume support even if the daemon was built with it.
+   ** Added new '--test-no-udev' option to disable the runtime udev support even
+      if the daemon was built with it.
+
+ * Serial:
+   ** Also match OK or ERROR responses that are not at end of line.
+
+ * SIM:
+   ** Force reprobing the modem if a new SIM is detected in a modem that
+      initially started in Failed state without SIM.
+   ** Force reprobing the modem if the lock status cannot be read after sending
+      SIM-PUK, so that it transitions to the Failed state.
+   ** Force reprobing the modem if a PUK lock is discovered after sending
+      SIM-PIN, so that it transitions to the Failed state.
+
+ * QMI:
+   ** The logic no longer depends on the service version reported by each
+      client, the support for each feature is explicitly probed instead.
+   ** Implemented SIM profile (eUICC) change detection.
+   ** Support for QMI modems on kernels < 3.6 is dropped. Only kernels where the
+      QMI control ports are exposed in the 'usbmisc' subsystem are supported.
+   ** Implemented additional step in the connection logic to allow binding the
+      WDS client to a given SIO port, required in the BAM-DMUX driver setup.
+   ** Implemented support for the initial EPS bearer settings logic.
+   ** Disabled explicit signal and access technology polling if indications have
+      been correctly enabled.
+
+ * MBIM:
+   ** Enable SIM hot swap detection logic with QMI over MBIM.
+   ** Allow plugins to specify explicitly that QMI over MBIM is not supported.
+
+ * libmm-glib:
+   ** Added missing APIs to get/set RM protocol in the Simple connect settings.
+
+ * Plugins:
+   ** gosuncn: new plugin, for now just with port type hints for the GM800.
+   ** quectel: implemented GPS support with +QGPS.
+   ** quectel: implemented custom time support check to prefer +CTZU=3 instead
+      of +CTZU=1 so that the modem reports localtime instead of UTC in +CCLK.
+   ** sierra: added support for XMM-specific features (e.g. EM7345).
+   ** cinterion: implemented support for the initial EPS bearer settings logic.
+   ** cinterion: added SIM hot swap support to AT-based modems.
+   ** huawei: updated to avoid applying multiple port type hint methods.
+   ** huawei: updated the ^GETPORTMODE logic so that we don't assume the hints
+      in the response apply to specific USB interfaces.
+
+The following features which were backported to 1.14.x releases are also present
+in ModemManager 1.16.0:
+
+ * location: allow CID only updates.
+ * sms: allow sending/receiving UTF-16 as if it were UCS-2.
+ * modem: don't consider charset setup failure as fatal.
+ * QMI: fix reporting signal strength indications.
+ * QMI: fix parsing of USSD indications with UTF-16 data.
+ * QMI: run network registration with NAS Set System Selection Preference.
+ * QMI: when connection aborted, ensure network handles are released.
+ * MBIM: don't fail IPv4v6 connection attempt if only IPv4 succeeds.
+ * cinterion: improve user/password handling in AT^SGAUTH calls.
+ * cinterion: removed limitation to IPv4 only PDP contexts.
+ * cinterion: configure the PLAS9 to send URCs correctly.
+ * quectel: add support for MBIM devices.
+ * telit: add initial delay for AT ports to become responsive.
+
+
 ModemManager 1.14.0
 -------------------------------------------
 This is a new stable release of ModemManager.
diff --git a/RELEASING b/RELEASING
index 8ae9a6e..cfe3ea2 100644
--- a/RELEASING
+++ b/RELEASING
@@ -2,6 +2,14 @@
 
 The ModemManager releases are generated using the GNU autotools.
 
+0.1) For major releases:
+     * Increment mm_minor_version and reset mm_micro_version.
+     * Assuming API/ABI compatibility in libmm-glib, increment both
+       mm_glib_lt_current and mm_glib_lt_age.
+
+0.2) For stable branch releases:
+     * Increment mm_micro_version.
+
 1) Configure and build the whole project, making sure gtk-doc is enabled:
 
     $ NOCONFIGURE=1 ./autogen.sh
@@ -24,9 +32,44 @@
     $ gpg --verify ModemManager-${VERSION}.tar.xz.asc ModemManager-${VERSION}.tar.xz
 
 5) Upload source tarball (.tar.xz) and signature (.tar.xz.asc) to
-   freedesktop.org
+   freedesktop.org.
+    $ scp ModemManager-${VERSION}.tar.xz* fd.o:${ModemManager}/
 
-TODO: manpages and gtk-doc references
+6) Create directories for the manpages and gtk-doc documentation in
+   freedesktop.org, and also update the 'latest' links:
+    $ ssh fd.o
+    [fd.o] $ cd ${ModemManager}/man/
+    [fd.o] $ rm latest
+    [fd.o] $ mkdir -p ${VERSION}
+    [fd.o] $ ln -s ${VERSION} latest
+    [fd.o] $ cd ${ModemManager}/doc/
+    [fd.o] $ rm latest
+    [fd.o] $ mkdir -p ${VERSION}/ModemManager
+    [fd.o] $ mkdir -p ${VERSION}/libmm-glib
+    [fd.o] $ ln -s ${VERSION} latest
+
+7) Generate HTML for the manpages:
+    $ roffit < docs/man/mmcli.1 > mmcli.1.html
+    $ roffit < docs/man/ModemManager.8 > ModemManager.8.html
+
+8) Upload manpages in HTML to freedesktop.org:
+    $ scp *.html fd.o:${ModemManager}/man/${VERSION}/
+
+9) Upload the gtk-doc in HTML available inside the source tarball to
+   freedesktop.org. It must be the one inside the tarball because it contains
+   the correct fixed refs to the online documentation of the dependencies
+   (e.g. the glib/gobject/gio documentation URLs in http://developer.gnome.org)
+    $ tar -Jxvf ModemManager-${VERSION}.tar.xz
+    $ scp ModemManager-${VERSION}/docs/reference/api/html/* fd.o:${ModemManager}/doc/${VERSION}/ModemManager/
+    $ scp ModemManager-${VERSION}/docs/reference/libmm-glib/html/* fd.o:${ModemManager}/doc/${VERSION}/libmm-glib/
+
+10.1) For major releases:
+     * Fork new stable branch (e.g. mm-${MAJOR}-${MINOR})
+     * Post-release version bump in the master branch, increment mm_minor_version.
+     * Post-release version bump in the stable branch, increment mm_micro_version.
+
+10.2) For stable branch releases:
+     * Post-release version bump, increment mm_micro_version.
 
 -------------------------------------------------------------------------------
 
@@ -34,4 +77,3 @@
    signed it, e.g.:
 
     $ curl https://www.freedesktop.org/software/ModemManager/0x3CAD53398973FFFA.asc | gpg --import
-
diff --git a/cli/mmcli-output.c b/cli/mmcli-output.c
index 275e706..3ef7fd1 100644
--- a/cli/mmcli-output.c
+++ b/cli/mmcli-output.c
@@ -273,6 +273,7 @@
     [MMC_F_SIM_PROPERTIES_OPERATOR_ID]        = { "sim.properties.operator-code",                    "operator id",              MMC_S_SIM_PROPERTIES,          },
     [MMC_F_SIM_PROPERTIES_OPERATOR_NAME]      = { "sim.properties.operator-name",                    "operator name",            MMC_S_SIM_PROPERTIES,          },
     [MMC_F_SIM_PROPERTIES_EMERGENCY_NUMBERS]  = { "sim.properties.emergency-numbers",                "emergency numbers",        MMC_S_SIM_PROPERTIES,          },
+    [MMC_F_SIM_PROPERTIES_PREFERRED_NETWORKS] = { "sim.properties.preferred-networks",               "preferred networks",       MMC_S_SIM_PROPERTIES,          },
     [MMC_F_MODEM_LIST_DBUS_PATH]              = { "modem-list",                                      "modems",                   MMC_S_UNKNOWN,                 },
     [MMC_F_SMS_LIST_DBUS_PATH]                = { "modem.messaging.sms",                             "sms messages",             MMC_S_UNKNOWN,                 },
     [MMC_F_CALL_LIST_DBUS_PATH]               = { "modem.voice.call",                                "calls",                    MMC_S_UNKNOWN,                 },
@@ -798,6 +799,44 @@
 }
 
 /******************************************************************************/
+/* (Custom) Preferred networks output */
+
+void
+mmcli_output_preferred_networks (GList *preferred_nets_list)
+{
+    if (preferred_nets_list) {
+        GPtrArray *aux;
+
+        aux = g_ptr_array_new ();
+        for (;preferred_nets_list; preferred_nets_list = g_list_next (preferred_nets_list)) {
+            const MMSimPreferredNetwork *preferred_net;
+            gchar       *access_technologies;
+            gchar       *out;
+            const gchar *operator_code;
+
+            preferred_net = (const MMSimPreferredNetwork *)preferred_nets_list->data;
+            operator_code = mm_sim_preferred_network_get_operator_code (preferred_net);
+            access_technologies = mm_modem_access_technology_build_string_from_mask (mm_sim_preferred_network_get_access_technology (preferred_net));
+
+            if (selected_type == MMC_OUTPUT_TYPE_HUMAN)
+                out = g_strdup_printf ("%s (%s)",
+                                       operator_code,
+                                       access_technologies);
+            else
+                out = g_strdup_printf ("operator-code: %s, access-technologies: %s",
+                                       operator_code,
+                                       access_technologies);
+
+            g_ptr_array_add (aux, out);
+            g_free (access_technologies);
+        }
+        g_ptr_array_add (aux, NULL);
+
+        mmcli_output_string_array_take (MMC_F_SIM_PROPERTIES_PREFERRED_NETWORKS, (gchar **) g_ptr_array_free (aux, FALSE), TRUE);
+    }
+}
+
+/******************************************************************************/
 /* Human-friendly output */
 
 #define HUMAN_MAX_VALUE_LENGTH 60
diff --git a/cli/mmcli-output.h b/cli/mmcli-output.h
index 928bcd3..dea575b 100644
--- a/cli/mmcli-output.h
+++ b/cli/mmcli-output.h
@@ -290,6 +290,7 @@
     MMC_F_SIM_PROPERTIES_OPERATOR_ID,
     MMC_F_SIM_PROPERTIES_OPERATOR_NAME,
     MMC_F_SIM_PROPERTIES_EMERGENCY_NUMBERS,
+    MMC_F_SIM_PROPERTIES_PREFERRED_NETWORKS,
     /* Lists */
     MMC_F_MODEM_LIST_DBUS_PATH,
     MMC_F_SMS_LIST_DBUS_PATH,
@@ -341,16 +342,17 @@
 /******************************************************************************/
 /* Custom output management */
 
-void mmcli_output_signal_quality   (guint                      value,
-                                    gboolean                   recent);
-void mmcli_output_state            (MMModemState               state,
-                                    MMModemStateFailedReason   reason);
-void mmcli_output_sim_slots        (gchar                    **sim_slot_paths,
-                                    guint                      primary_sim_slot);
-void mmcli_output_scan_networks    (GList                     *network_list);
-void mmcli_output_firmware_list    (GList                     *firmware_list,
-                                    MMFirmwareProperties      *selected);
-void mmcli_output_pco_list         (GList                     *pco_list);
+void mmcli_output_signal_quality     (guint                      value,
+                                      gboolean                   recent);
+void mmcli_output_state              (MMModemState               state,
+                                      MMModemStateFailedReason   reason);
+void mmcli_output_sim_slots          (gchar                    **sim_slot_paths,
+                                      guint                      primary_sim_slot);
+void mmcli_output_scan_networks      (GList                     *network_list);
+void mmcli_output_firmware_list      (GList                     *firmware_list,
+                                      MMFirmwareProperties      *selected);
+void mmcli_output_pco_list           (GList                     *pco_list);
+void mmcli_output_preferred_networks (GList                     *preferred_nets_list);
 
 /******************************************************************************/
 /* Dump output */
diff --git a/cli/mmcli-sim.c b/cli/mmcli-sim.c
index 2a1fed3..68ae7f5 100644
--- a/cli/mmcli-sim.c
+++ b/cli/mmcli-sim.c
@@ -158,6 +158,8 @@
 static void
 print_sim_info (MMSim *sim)
 {
+    GList *preferred_nets_list;
+
     mmcli_output_string       (MMC_F_SIM_GENERAL_DBUS_PATH,            mm_sim_get_path (sim));
     mmcli_output_string       (MMC_F_SIM_PROPERTIES_ACTIVE,            mm_sim_get_active (sim) ? "yes" : "no");
     mmcli_output_string       (MMC_F_SIM_PROPERTIES_IMSI,              mm_sim_get_imsi (sim));
@@ -166,6 +168,9 @@
     mmcli_output_string       (MMC_F_SIM_PROPERTIES_OPERATOR_ID,       mm_sim_get_operator_identifier (sim));
     mmcli_output_string       (MMC_F_SIM_PROPERTIES_OPERATOR_NAME,     mm_sim_get_operator_name (sim));
     mmcli_output_string_array (MMC_F_SIM_PROPERTIES_EMERGENCY_NUMBERS, (const gchar **) mm_sim_get_emergency_numbers (sim), FALSE);
+    preferred_nets_list = mm_sim_get_preferred_networks (sim);
+    mmcli_output_preferred_networks (preferred_nets_list);
+    g_list_free_full (preferred_nets_list, (GDestroyNotify) mm_sim_preferred_network_free);
     mmcli_output_dump ();
 }
 
diff --git a/configure.ac b/configure.ac
index 8c2711f..be1b33f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -5,7 +5,7 @@
 dnl
 
 m4_define([mm_major_version], [1])
-m4_define([mm_minor_version], [15])
+m4_define([mm_minor_version], [17])
 m4_define([mm_micro_version], [0])
 m4_define([mm_version],
           [mm_major_version.mm_minor_version.mm_micro_version])
@@ -18,9 +18,9 @@
 dnl            with old code), increment a.
 dnl        If the interface has changed in an incompatible way (that is,
 dnl            functions have changed or been removed), then zero a.
-m4_define([mm_glib_lt_current],  [6])
+m4_define([mm_glib_lt_current],  [7])
 m4_define([mm_glib_lt_revision], [0])
-m4_define([mm_glib_lt_age],      [6])
+m4_define([mm_glib_lt_age],      [7])
 
 dnl-----------------------------------------------------------------------------
 dnl autoconf, automake, libtool initialization
@@ -401,7 +401,7 @@
 dnl QMI support (enabled by default)
 dnl
 
-LIBQMI_VERSION=1.27.3
+LIBQMI_VERSION=1.28.0
 
 AC_ARG_WITH(qmi, AS_HELP_STRING([--without-qmi], [Build without QMI support]), [], [with_qmi=yes])
 AM_CONDITIONAL(WITH_QMI, test "x$with_qmi" = "xyes")
diff --git a/docs/reference/libmm-glib/libmm-glib-docs.xml b/docs/reference/libmm-glib/libmm-glib-docs.xml
index 71e7440..a67a0e3 100644
--- a/docs/reference/libmm-glib/libmm-glib-docs.xml
+++ b/docs/reference/libmm-glib/libmm-glib-docs.xml
@@ -153,6 +153,7 @@
     <chapter>
       <title>The SIM object</title>
       <xi:include href="xml/mm-sim.xml"/>
+      <xi:include href="xml/mm-sim-preferred-network.xml"/>
     </chapter>
 
     <chapter>
@@ -298,6 +299,10 @@
     <title>Index of new symbols in 1.16</title>
     <xi:include href="xml/api-index-1.16.xml"></xi:include>
   </chapter>
+  <chapter id="api-index-1-18" role="1.18">
+    <title>Index of new symbols in 1.18</title>
+    <xi:include href="xml/api-index-1.18.xml"></xi:include>
+  </chapter>
 
   <xi:include href="xml/annotation-glossary.xml"></xi:include>
 </book>
diff --git a/docs/reference/libmm-glib/libmm-glib-sections.txt b/docs/reference/libmm-glib/libmm-glib-sections.txt
index 0a72bef..c1daf23 100644
--- a/docs/reference/libmm-glib/libmm-glib-sections.txt
+++ b/docs/reference/libmm-glib/libmm-glib-sections.txt
@@ -1215,6 +1215,25 @@
 </SECTION>
 
 <SECTION>
+<FILE>mm-sim-preferred-network</FILE>
+<TITLE>MMSimPreferredNetwork</TITLE>
+MMSimPreferredNetwork
+mm_sim_preferred_network_get_operator_code
+mm_sim_preferred_network_get_access_technology
+mm_sim_preferred_network_free
+<SUBSECTION Private>
+mm_sim_preferred_network_new
+mm_sim_preferred_network_new_from_variant
+mm_sim_preferred_network_set_access_technology
+mm_sim_preferred_network_set_operator_code
+mm_sim_preferred_network_get_tuple
+mm_sim_preferred_network_list_get_variant
+<SUBSECTION Standard>
+MM_TYPE_SIM_PREFERRED_NETWORK
+mm_sim_preferred_network_get_type
+</SECTION>
+
+<SECTION>
 <FILE>mm-sim</FILE>
 <TITLE>MMSim</TITLE>
 MMSim
@@ -1234,6 +1253,7 @@
 mm_sim_dup_operator_name
 mm_sim_get_emergency_numbers
 mm_sim_dup_emergency_numbers
+mm_sim_get_preferred_networks
 <SUBSECTION Methods>
 mm_sim_send_pin
 mm_sim_send_pin_finish
@@ -3125,6 +3145,8 @@
 mm_gdbus_sim_dup_operator_name
 mm_gdbus_sim_get_emergency_numbers
 mm_gdbus_sim_dup_emergency_numbers
+mm_gdbus_sim_dup_preferred_networks
+mm_gdbus_sim_get_preferred_networks
 <SUBSECTION Methods>
 mm_gdbus_sim_call_send_pin
 mm_gdbus_sim_call_send_pin_finish
@@ -3146,6 +3168,7 @@
 mm_gdbus_sim_set_operator_name
 mm_gdbus_sim_set_sim_identifier
 mm_gdbus_sim_set_emergency_numbers
+mm_gdbus_sim_set_preferred_networks
 mm_gdbus_sim_complete_change_pin
 mm_gdbus_sim_complete_enable_pin
 mm_gdbus_sim_complete_send_pin
diff --git a/examples/network-scan-python/network-scan-python b/examples/network-scan-python/network-scan-python
index 12ee02f..21dd89f 100755
--- a/examples/network-scan-python/network-scan-python
+++ b/examples/network-scan-python/network-scan-python
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # -*- Mode: python; 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
@@ -26,7 +26,8 @@
 from gi.repository import Gio, GLib, GObject, ModemManager
 
 
-if __name__ == "__main__":
+def main():
+    """Main routine."""
 
     # Process input arguments
     if len(sys.argv) != 1:
@@ -67,3 +68,7 @@
                         network.get_availability())))
         else:
             print('no networks found')
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/sms-python/sms-python b/examples/sms-python/sms-python
index 569db37..46d5e69 100755
--- a/examples/sms-python/sms-python
+++ b/examples/sms-python/sms-python
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # -*- Mode: python; 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
@@ -25,7 +25,8 @@
 from gi.repository import Gio, GLib, GObject, ModemManager
 
 
-if __name__ == "__main__":
+def main():
+    """Main routine."""
 
     # Process input arguments
     if len(sys.argv) != 3:
@@ -34,14 +35,15 @@
         sys.exit(1)
 
     # Prepare SMS properties
-    sms_properties = ModemManager.SmsProperties.new ()
+    sms_properties = ModemManager.SmsProperties.new()
     sms_properties.set_number(sys.argv[1])
     sms_properties.set_text(sys.argv[2])
 
     # Connection to ModemManager
-    connection = Gio.bus_get_sync (Gio.BusType.SYSTEM, None)
-    manager = ModemManager.Manager.new_sync (connection, Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, None)
-    if manager.get_name_owner() is None:
+    connection = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+    manager = ModemManager.Manager.new_sync(
+        connection, Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, None)
+    if not manager.get_name_owner():
         sys.stderr.write('ModemManager not found in bus')
         sys.exit(2)
 
@@ -51,3 +53,7 @@
         sms = messaging.create_sync(sms_properties)
         sms.send_sync()
         print('%s: sms sent' % messaging.get_object_path())
+
+
+if __name__ == "__main__":
+    main()
diff --git a/introspection/org.freedesktop.ModemManager1.Sim.xml b/introspection/org.freedesktop.ModemManager1.Sim.xml
index 87891e3..63d0e9b 100644
--- a/introspection/org.freedesktop.ModemManager1.Sim.xml
+++ b/introspection/org.freedesktop.ModemManager1.Sim.xml
@@ -127,5 +127,19 @@
     -->
     <property name="EmergencyNumbers" type="as" access="read" />
 
+    <!--
+        PreferredNetworks:
+
+        List of preferred networks with access technologies configured in the SIM card.
+
+        Each entry contains an operator id string (<literal>"MCCMNC"</literal>)
+        consisting of 5 or 6 digits, and an
+        <link linkend="MMModemAccessTechnology">MMModemAccessTechnology</link> mask.
+        If the SIM card does not support access technology storage, the mask will be
+        set to <link linkend="MM-MODEM-ACCESS-TECHNOLOGY-UNKNOWN:CAPS">
+        MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN</link>.
+    -->
+    <property name="PreferredNetworks" type="a(su)" access="read" />
+
   </interface>
 </node>
diff --git a/libmm-glib/Makefile.am b/libmm-glib/Makefile.am
index 49f1e83..f33836f 100644
--- a/libmm-glib/Makefile.am
+++ b/libmm-glib/Makefile.am
@@ -89,6 +89,8 @@
 	mm-pco.c \
 	mm-call-audio-format.h \
 	mm-call-audio-format.c \
+	mm-sim-preferred-network.h \
+	mm-sim-preferred-network.c \
 	$(NULL)
 
 libmm_glib_la_CPPFLAGS = \
@@ -160,6 +162,7 @@
 	mm-kernel-event-properties.h \
 	mm-pco.h \
 	mm-call-audio-format.h \
+	mm-sim-preferred-network.h \
 	$(NULL)
 
 CLEANFILES =
diff --git a/libmm-glib/libmm-glib.h b/libmm-glib/libmm-glib.h
index e450489..6572b2d 100644
--- a/libmm-glib/libmm-glib.h
+++ b/libmm-glib/libmm-glib.h
@@ -80,6 +80,7 @@
 #include <mm-signal.h>
 #include <mm-kernel-event-properties.h>
 #include <mm-pco.h>
+#include <mm-sim-preferred-network.h>
 
 /* generated */
 #include <mm-errors-types.h>
diff --git a/libmm-glib/mm-bearer-properties.c b/libmm-glib/mm-bearer-properties.c
index f3ccddb..4305cec 100644
--- a/libmm-glib/mm-bearer-properties.c
+++ b/libmm-glib/mm-bearer-properties.c
@@ -761,7 +761,7 @@
     if (!(flags & MM_BEARER_PROPERTIES_CMP_FLAGS_NO_ALLOW_ROAMING)) {
         if (a->priv->allow_roaming != b->priv->allow_roaming)
             return FALSE;
-        if (a->priv->allow_roaming_set != b->priv->allow_roaming)
+        if (a->priv->allow_roaming_set != b->priv->allow_roaming_set)
             return FALSE;
     }
     if (a->priv->rm_protocol != b->priv->rm_protocol)
diff --git a/libmm-glib/mm-common-helpers.c b/libmm-glib/mm-common-helpers.c
index da1cc1c..43fff92 100644
--- a/libmm-glib/mm-common-helpers.c
+++ b/libmm-glib/mm-common-helpers.c
@@ -1686,33 +1686,48 @@
     return (a << 4) | b;
 }
 
-gchar *
-mm_utils_hexstr2bin (const gchar *hex, gsize *out_len)
+guint8 *
+mm_utils_hexstr2bin (const gchar  *hex,
+                     gssize        len,
+                     gsize        *out_len,
+                     GError      **error)
 {
     const gchar *ipos = hex;
-    gchar *buf = NULL;
+    g_autofree guint8 *buf = NULL;
     gsize i;
     gint a;
-    gchar *opos;
-    gsize len;
+    guint8 *opos;
 
-    len = strlen (hex);
+    if (len < 0)
+        len = strlen (hex);
+
+    if (len == 0) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Hex conversion failed: empty string");
+        return NULL;
+    }
 
     /* Length must be a multiple of 2 */
-    g_return_val_if_fail ((len % 2) == 0, NULL);
+    if ((len % 2) != 0) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Hex conversion failed: invalid input length");
+        return NULL;
+    }
 
-    opos = buf = g_malloc0 ((len / 2) + 1);
+    opos = buf = g_malloc0 (len / 2);
     for (i = 0; i < len; i += 2) {
         a = mm_utils_hex2byte (ipos);
         if (a < 0) {
-            g_free (buf);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                         "Hex byte conversion from '%c%c' failed",
+                         ipos[0], ipos[1]);
             return NULL;
         }
-        *opos++ = a;
+        *opos++ = (guint8)a;
         ipos += 2;
     }
     *out_len = len / 2;
-    return buf;
+    return g_steal_pointer (&buf);
 }
 
 /* End from hostap */
@@ -1723,9 +1738,9 @@
     gsize len;
     gsize i;
 
-    /* Length not multiple of 2? */
+    /* Empty string or length not multiple of 2? */
     len = strlen (hex);
-    if (len % 2 != 0)
+    if (len == 0 || (len % 2) != 0)
         return FALSE;
 
     for (i = 0; i < len; i++) {
diff --git a/libmm-glib/mm-common-helpers.h b/libmm-glib/mm-common-helpers.h
index 65d0e70..4e6d978 100644
--- a/libmm-glib/mm-common-helpers.h
+++ b/libmm-glib/mm-common-helpers.h
@@ -181,7 +181,7 @@
 const gchar *mm_sms_delivery_state_get_string_extended (guint delivery_state);
 
 gint      mm_utils_hex2byte   (const gchar *hex);
-gchar    *mm_utils_hexstr2bin (const gchar *hex, gsize *out_len);
+guint8   *mm_utils_hexstr2bin (const gchar *hex, gssize len, gsize *out_len, GError **error);
 gchar    *mm_utils_bin2hexstr (const guint8 *bin, gsize len);
 gboolean  mm_utils_ishexstr   (const gchar *hex);
 
diff --git a/libmm-glib/mm-sim-preferred-network.c b/libmm-glib/mm-sim-preferred-network.c
new file mode 100644
index 0000000..c53239d
--- /dev/null
+++ b/libmm-glib/mm-sim-preferred-network.c
@@ -0,0 +1,167 @@
+/* -*- 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) 2021 UROS Ltd
+ */
+
+#include "mm-sim-preferred-network.h"
+
+struct _MMSimPreferredNetwork {
+    gchar *operator_code;
+    MMModemAccessTechnology access_technology;
+};
+
+static MMSimPreferredNetwork *
+mm_sim_preferred_network_copy (MMSimPreferredNetwork *preferred_network)
+{
+    MMSimPreferredNetwork *preferred_network_copy;
+
+    preferred_network_copy = g_slice_new0 (MMSimPreferredNetwork);
+    preferred_network_copy->operator_code     = g_strdup (preferred_network->operator_code);
+    preferred_network_copy->access_technology = preferred_network->access_technology;
+
+    return preferred_network_copy;
+}
+
+G_DEFINE_BOXED_TYPE (MMSimPreferredNetwork, mm_sim_preferred_network, (GBoxedCopyFunc) mm_sim_preferred_network_copy, (GBoxedFreeFunc) mm_sim_preferred_network_free)
+
+/**
+ * mm_sim_preferred_network_free:
+ * @self: A #MMSimPreferredNetwork.
+ *
+ * Frees a #MMSimPreferredNetwork.
+ *
+ * Since: 1.18
+ */
+void
+mm_sim_preferred_network_free (MMSimPreferredNetwork *self)
+{
+    if (!self)
+        return;
+
+    g_free (self->operator_code);
+    g_slice_free (MMSimPreferredNetwork, self);
+}
+
+/**
+ * mm_sim_preferred_network_get_operator_code:
+ * @self: A #MMSimPreferredNetwork.
+ *
+ * Get the operator code (MCCMNC) of the preferred network.
+ *
+ * Returns: (transfer none): The operator code, or %NULL if none available.
+ *
+ * Since: 1.18
+ */
+const gchar *
+mm_sim_preferred_network_get_operator_code (const MMSimPreferredNetwork *self)
+{
+    g_return_val_if_fail (self != NULL, NULL);
+
+    return self->operator_code;
+}
+
+/**
+ * mm_sim_preferred_network_get_access_technology:
+ * @self: A #MMSimPreferredNetwork.
+ *
+ * Get the access technology mask of the preferred network.
+ *
+ * Returns: A #MMModemAccessTechnology.
+ *
+ * Since: 1.18
+ */
+MMModemAccessTechnology
+mm_sim_preferred_network_get_access_technology (const MMSimPreferredNetwork *self)
+{
+    g_return_val_if_fail (self != NULL, MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN);
+
+    return self->access_technology;
+}
+
+/**
+ * mm_sim_preferred_network_set_operator_code: (skip)
+ */
+void
+mm_sim_preferred_network_set_operator_code (MMSimPreferredNetwork *self,
+                                            const gchar *operator_code)
+{
+    g_return_if_fail (self != NULL);
+
+    g_free (self->operator_code);
+    self->operator_code = g_strdup (operator_code);
+}
+
+/**
+ * mm_sim_preferred_network_set_access_technology: (skip)
+ */
+void
+mm_sim_preferred_network_set_access_technology (MMSimPreferredNetwork *self,
+                                                MMModemAccessTechnology access_technology)
+{
+    g_return_if_fail (self != NULL);
+
+    self->access_technology = access_technology;
+}
+
+/**
+ * mm_sim_preferred_network_new: (skip)
+ */
+MMSimPreferredNetwork *
+mm_sim_preferred_network_new (void)
+{
+    return g_slice_new0 (MMSimPreferredNetwork);
+}
+
+/**
+ * mm_sim_preferred_network_new_from_variant: (skip)
+ */
+MMSimPreferredNetwork *
+mm_sim_preferred_network_new_from_variant (GVariant *variant)
+{
+    MMSimPreferredNetwork *preferred_net;
+
+    g_return_val_if_fail (g_variant_is_of_type (variant, G_VARIANT_TYPE ("(su)")),
+                          NULL);
+
+    preferred_net = mm_sim_preferred_network_new ();
+    g_variant_get (variant, "(su)", &preferred_net->operator_code, &preferred_net->access_technology);
+    return preferred_net;
+}
+
+/**
+ * mm_sim_preferred_network_get_tuple: (skip)
+ */
+GVariant *
+mm_sim_preferred_network_get_tuple (const MMSimPreferredNetwork *self)
+{
+    return g_variant_new ("(su)",
+                          self->operator_code,
+                          self->access_technology);
+}
+
+/**
+ * mm_sim_preferred_network_list_get_variant: (skip)
+ */
+GVariant *
+mm_sim_preferred_network_list_get_variant (const GList *preferred_network_list)
+{
+    GVariantBuilder  builder;
+    const GList     *iter;
+
+    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(su)"));
+    for (iter = preferred_network_list; iter; iter = g_list_next (iter)) {
+        g_variant_builder_add_value (&builder,
+                                     mm_sim_preferred_network_get_tuple ((const MMSimPreferredNetwork *) iter->data));
+    }
+    return g_variant_builder_end (&builder);
+}
diff --git a/libmm-glib/mm-sim-preferred-network.h b/libmm-glib/mm-sim-preferred-network.h
new file mode 100644
index 0000000..e604258
--- /dev/null
+++ b/libmm-glib/mm-sim-preferred-network.h
@@ -0,0 +1,68 @@
+/* -*- 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) 2021 UROS Ltd
+ */
+
+#ifndef MM_SIM_PREFERRED_NETWORK_H
+#define MM_SIM_PREFERRED_NETWORK_H
+
+#if !defined (__LIBMM_GLIB_H_INSIDE__) && !defined (LIBMM_GLIB_COMPILATION)
+#error "Only <libmm-glib.h> can be included directly."
+#endif
+
+#include <ModemManager.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * MMSimPreferredNetwork:
+ *
+ * The #MMSimPreferredNetwork structure contains private data and should only be accessed
+ * using the provided API.
+ */
+typedef struct _MMSimPreferredNetwork MMSimPreferredNetwork;
+
+#define MM_TYPE_SIM_PREFERRED_NETWORK (mm_sim_preferred_network_get_type ())
+GType mm_sim_preferred_network_get_type (void);
+
+const gchar                    *mm_sim_preferred_network_get_operator_code     (const MMSimPreferredNetwork *self);
+MMModemAccessTechnology         mm_sim_preferred_network_get_access_technology (const MMSimPreferredNetwork *self);
+
+void                            mm_sim_preferred_network_free                  (MMSimPreferredNetwork *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMSimPreferredNetwork, mm_sim_preferred_network_free)
+
+/*****************************************************************************/
+/* ModemManager/libmm-glib/mmcli specific methods */
+
+#if defined (_LIBMM_INSIDE_MM) ||    \
+    defined (_LIBMM_INSIDE_MMCLI) || \
+    defined (LIBMM_GLIB_COMPILATION)
+
+MMSimPreferredNetwork *         mm_sim_preferred_network_new                   (void);
+MMSimPreferredNetwork *         mm_sim_preferred_network_new_from_variant      (GVariant *variant);
+
+void                            mm_sim_preferred_network_set_operator_code     (MMSimPreferredNetwork *self,
+                                                                                const gchar *operator_code);
+void                            mm_sim_preferred_network_set_access_technology (MMSimPreferredNetwork *self,
+                                                                                MMModemAccessTechnology access_technology);
+
+GVariant *mm_sim_preferred_network_get_tuple        (const MMSimPreferredNetwork *self);
+GVariant *mm_sim_preferred_network_list_get_variant (const GList *preferred_network_list);
+
+#endif
+
+G_END_DECLS
+
+#endif /* MM_SIM_PREFERRED_NETWORK_H */
diff --git a/libmm-glib/mm-sim.c b/libmm-glib/mm-sim.c
index 0a3b607..ac509d3 100644
--- a/libmm-glib/mm-sim.c
+++ b/libmm-glib/mm-sim.c
@@ -23,6 +23,7 @@
 
 #include "mm-helpers.h"
 #include "mm-sim.h"
+#include "mm-sim-preferred-network.h"
 
 /**
  * SECTION: mm-sim
@@ -860,6 +861,47 @@
 
 /*****************************************************************************/
 
+/**
+ * mm_sim_get_preferred_networks:
+ * @self: A #MMSim.
+ *
+ * Gets the list of #MMSimPreferredNetwork objects exposed by this
+ * #MMSim.
+ *
+ * Returns: (transfer full) (element-type ModemManager.SimPreferredNetwork): a list of
+ * #MMSimPreferredNetwork objects, or #NULL. The returned value should
+ * be freed with g_list_free_full() using mm_sim_preferred_network_free() as #GDestroyNotify
+ * function.
+ *
+ * Since: 1.18
+ */
+GList *
+mm_sim_get_preferred_networks (MMSim *self)
+{
+    GList *network_list = NULL;
+    GVariant *container, *child;
+    GVariantIter iter;
+
+    g_return_val_if_fail (MM_IS_SIM (self), NULL);
+
+    container = mm_gdbus_sim_get_preferred_networks (MM_GDBUS_SIM (self));
+    g_return_val_if_fail (g_variant_is_of_type (container, G_VARIANT_TYPE ("a(su)")), NULL);
+
+    g_variant_iter_init (&iter, container);
+    while ((child = g_variant_iter_next_value (&iter))) {
+        MMSimPreferredNetwork *preferred_net;
+
+        preferred_net = mm_sim_preferred_network_new_from_variant (child);
+        if (preferred_net)
+            network_list = g_list_append (network_list, preferred_net);
+        g_variant_unref (child);
+    }
+
+    return network_list;
+}
+
+/*****************************************************************************/
+
 static void
 mm_sim_init (MMSim *self)
 {
diff --git a/libmm-glib/mm-sim.h b/libmm-glib/mm-sim.h
index 3449e28..8ebf069 100644
--- a/libmm-glib/mm-sim.h
+++ b/libmm-glib/mm-sim.h
@@ -87,6 +87,8 @@
 const gchar * const  *mm_sim_get_emergency_numbers (MMSim *self);
 gchar               **mm_sim_dup_emergency_numbers (MMSim *self);
 
+GList*       mm_sim_get_preferred_networks  (MMSim *self);
+
 void     mm_sim_send_pin        (MMSim *self,
                                  const gchar *pin,
                                  GCancellable *cancellable,
diff --git a/libmm-glib/tests/test-common-helpers.c b/libmm-glib/tests/test-common-helpers.c
index 78432be..19e1122 100644
--- a/libmm-glib/tests/test-common-helpers.c
+++ b/libmm-glib/tests/test-common-helpers.c
@@ -13,6 +13,7 @@
  * Copyright (C) 2012 Google, Inc.
  */
 
+#include <string.h>
 #include <glib-object.h>
 
 #include <libmm-glib.h>
@@ -502,6 +503,102 @@
 }
 
 /**************************************************************/
+/* hexstr2bin & bin2hexstr */
+
+static void
+common_hexstr2bin_test_failure (const gchar *input_hex)
+{
+    g_autoptr(GError)  error = NULL;
+    g_autofree guint8 *bin = NULL;
+    gsize              bin_len = 0;
+
+    g_assert (mm_utils_ishexstr (input_hex) == FALSE);
+
+    bin = mm_utils_hexstr2bin (input_hex, -1, &bin_len, &error);
+    g_assert_null (bin);
+    g_assert_nonnull (error);
+}
+
+static void
+common_hexstr2bin_test_success_len (const gchar *input_hex,
+                                    gssize       input_hex_len)
+{
+    g_autoptr(GError)  error = NULL;
+    g_autofree guint8 *bin = NULL;
+    gsize              bin_len = 0;
+    g_autofree gchar  *hex = NULL;
+
+    bin = mm_utils_hexstr2bin (input_hex, input_hex_len, &bin_len, &error);
+    g_assert_nonnull (bin);
+    g_assert_no_error (error);
+
+    hex = mm_utils_bin2hexstr (bin, bin_len);
+    g_assert_nonnull (hex);
+
+    if (input_hex_len == -1)
+        g_assert (g_ascii_strcasecmp (input_hex, hex) == 0);
+    else
+        g_assert (g_ascii_strncasecmp (input_hex, hex, (gsize)input_hex_len) == 0);
+}
+
+static void
+common_hexstr2bin_test_success (const gchar *input_hex)
+{
+    gsize  input_hex_len;
+    gssize i;
+
+    g_assert (mm_utils_ishexstr (input_hex) == TRUE);
+
+    common_hexstr2bin_test_success_len (input_hex, -1);
+
+    input_hex_len = strlen (input_hex);
+    for (i = input_hex_len; i >= 2; i-=2)
+        common_hexstr2bin_test_success_len (input_hex, i);
+}
+
+static void
+hexstr_lower_case (void)
+{
+    common_hexstr2bin_test_success ("000123456789abcdefff");
+}
+
+static void
+hexstr_upper_case (void)
+{
+    common_hexstr2bin_test_success ("000123456789ABCDEFFF");
+}
+
+static void
+hexstr_mixed_case (void)
+{
+    common_hexstr2bin_test_success ("000123456789AbcDefFf");
+}
+
+static void
+hexstr_empty (void)
+{
+    common_hexstr2bin_test_failure ("");
+}
+
+static void
+hexstr_missing_digits (void)
+{
+    common_hexstr2bin_test_failure ("012");
+}
+
+static void
+hexstr_wrong_digits_all (void)
+{
+    common_hexstr2bin_test_failure ("helloworld");
+}
+
+static void
+hexstr_wrong_digits_some (void)
+{
+    common_hexstr2bin_test_failure ("012345k7");
+}
+
+/**************************************************************/
 
 int main (int argc, char **argv)
 {
@@ -539,5 +636,13 @@
     g_test_add_func ("/MM/Common/FieldParsers/Uint", field_parser_uint);
     g_test_add_func ("/MM/Common/FieldParsers/Double", field_parser_double);
 
+    g_test_add_func ("/MM/Common/HexStr/lower-case",        hexstr_lower_case);
+    g_test_add_func ("/MM/Common/HexStr/upper-case",        hexstr_upper_case);
+    g_test_add_func ("/MM/Common/HexStr/mixed-case",        hexstr_mixed_case);
+    g_test_add_func ("/MM/Common/HexStr/missing-empty",     hexstr_empty);
+    g_test_add_func ("/MM/Common/HexStr/missing-digits",    hexstr_missing_digits);
+    g_test_add_func ("/MM/Common/HexStr/wrong-digits-all",  hexstr_wrong_digits_all);
+    g_test_add_func ("/MM/Common/HexStr/wrong-digits-some", hexstr_wrong_digits_some);
+
     return g_test_run ();
 }
diff --git a/plugins/altair/mm-broadband-modem-altair-lte.c b/plugins/altair/mm-broadband-modem-altair-lte.c
index d26335f..6aebbd9 100644
--- a/plugins/altair/mm-broadband-modem-altair-lte.c
+++ b/plugins/altair/mm-broadband-modem-altair-lte.c
@@ -1067,7 +1067,7 @@
                                            error))
         return NULL;
 
-    mm_3gpp_normalize_operator (&operator_name, MM_MODEM_CHARSET_UNKNOWN);
+    mm_3gpp_normalize_operator (&operator_name, MM_MODEM_CHARSET_UNKNOWN, self);
     return operator_name;
 }
 
diff --git a/plugins/altair/mm-modem-helpers-altair-lte.c b/plugins/altair/mm-modem-helpers-altair-lte.c
index 278f31e..d2fd9af 100644
--- a/plugins/altair/mm-modem-helpers-altair-lte.c
+++ b/plugins/altair/mm-modem-helpers-altair-lte.c
@@ -155,9 +155,9 @@
                          G_REGEX_DOLLAR_ENDONLY | G_REGEX_RAW,
                          0, NULL);
     g_assert (regex);
-    if (!g_regex_match_full (regex, pco_info, strlen (pco_info), 0, 0, &match_info, error)) {
+
+    if (!g_regex_match_full (regex, pco_info, strlen (pco_info), 0, 0, &match_info, error))
         return NULL;
-    }
 
     num_matches = g_match_info_get_match_count (match_info);
     if (num_matches != 5) {
@@ -170,22 +170,18 @@
     }
 
     while (g_match_info_matches (match_info)) {
-        guint pco_cid;
-        gchar *pco_id;
-        gchar *pco_payload;
-        gsize pco_payload_len;
-        gchar *pco_payload_bytes = NULL;
-        gsize pco_payload_bytes_len;
-        guint8 pco_prefix[6];
-        GByteArray *pco_raw;
-        gsize pco_raw_len;
+        guint              pco_cid;
+        g_autofree gchar  *pco_id = NULL;
+        g_autofree gchar  *pco_payload = NULL;
+        g_autofree guint8 *pco_payload_bytes = NULL;
+        gsize              pco_payload_bytes_len;
+        guint8             pco_prefix[6];
+        GByteArray        *pco_raw;
+        gsize              pco_raw_len;
 
         if (!mm_get_uint_from_match_info (match_info, 1, &pco_cid)) {
-            g_set_error (error,
-                         MM_CORE_ERROR,
-                         MM_CORE_ERROR_FAILED,
-                         "Couldn't parse CID from PCO info: '%s'",
-                         pco_info);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "Couldn't parse CID from PCO info: '%s'", pco_info);
             break;
         }
 
@@ -197,42 +193,26 @@
 
         pco_id = mm_get_string_unquoted_from_match_info (match_info, 3);
         if (!pco_id) {
-            g_set_error (error,
-                         MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                         "Couldn't parse PCO ID from PCO info: '%s'",
-                         pco_info);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "Couldn't parse PCO ID from PCO info: '%s'", pco_info);
             break;
         }
 
         if (g_strcmp0 (pco_id, "FF00")) {
-            g_free (pco_id);
             g_match_info_next (match_info, error);
             continue;
         }
-        g_free (pco_id);
 
         pco_payload = mm_get_string_unquoted_from_match_info (match_info, 4);
         if (!pco_payload) {
-            g_set_error (error,
-                         MM_CORE_ERROR,
-                         MM_CORE_ERROR_FAILED,
-                         "Couldn't parse PCO payload from PCO info: '%s'",
-                         pco_info);
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "Couldn't parse PCO payload from PCO info: '%s'", pco_info);
             break;
         }
 
-        pco_payload_len = strlen (pco_payload);
-        if (pco_payload_len % 2 == 0)
-            pco_payload_bytes = mm_utils_hexstr2bin (pco_payload, &pco_payload_bytes_len);
-
-        g_free (pco_payload);
-
+        pco_payload_bytes = mm_utils_hexstr2bin (pco_payload, -1, &pco_payload_bytes_len, error);
         if (!pco_payload_bytes) {
-            g_set_error (error,
-                         MM_CORE_ERROR,
-                         MM_CORE_ERROR_FAILED,
-                         "Invalid PCO payload from PCO info: '%s'",
-                         pco_info);
+            g_prefix_error (error, "Invalid PCO payload from PCO info '%s': ", pco_info);
             break;
         }
 
@@ -266,14 +246,12 @@
 
         pco_raw = g_byte_array_sized_new (pco_raw_len);
         g_byte_array_append (pco_raw, pco_prefix, sizeof (pco_prefix));
-        g_byte_array_append (pco_raw, (guint8 *)pco_payload_bytes, pco_payload_bytes_len);
-        g_free (pco_payload_bytes);
+        g_byte_array_append (pco_raw, pco_payload_bytes, pco_payload_bytes_len);
 
         pco = mm_pco_new ();
         mm_pco_set_session_id (pco, pco_cid);
         mm_pco_set_complete (pco, TRUE);
         mm_pco_set_data (pco, pco_raw->data, pco_raw->len);
-        g_byte_array_unref (pco_raw);
         break;
     }
 
diff --git a/plugins/cinterion/mm-broadband-bearer-cinterion.c b/plugins/cinterion/mm-broadband-bearer-cinterion.c
index 464e75e..4c59d56 100644
--- a/plugins/cinterion/mm-broadband-bearer-cinterion.c
+++ b/plugins/cinterion/mm-broadband-bearer-cinterion.c
@@ -63,7 +63,7 @@
     guint usb_iface_num;
     guint i;
 
-    usb_iface_num = mm_kernel_device_get_property_as_int_hex (mm_port_peek_kernel_device (data), "ID_USB_INTERFACE_NUM");
+    usb_iface_num = (guint) mm_kernel_device_get_interface_number (mm_port_peek_kernel_device (data));
 
     for (i = 0; i < G_N_ELEMENTS (usb_interface_configs); i++) {
         if (usb_interface_configs[i].usb_iface_num == usb_iface_num)
diff --git a/plugins/cinterion/mm-broadband-modem-cinterion.c b/plugins/cinterion/mm-broadband-modem-cinterion.c
index 4986eea..7eeb8ad 100644
--- a/plugins/cinterion/mm-broadband-modem-cinterion.c
+++ b/plugins/cinterion/mm-broadband-modem-cinterion.c
@@ -107,9 +107,6 @@
 
     /* Initial EPS bearer context number */
     gint initial_eps_bearer_cid;
-
-    /* Command sequence */
-    MMBaseModemAtCommandAlloc *cmds;
 };
 
 /*****************************************************************************/
@@ -2070,6 +2067,23 @@
 /*****************************************************************************/
 /* Set current bands (Modem interface) */
 
+typedef struct {
+    MMBaseModemAtCommandAlloc *cmds;
+} SetCurrentBandsContext;
+
+static void
+set_current_bands_context_free (SetCurrentBandsContext *ctx)
+{
+    if (ctx->cmds) {
+        guint i;
+
+        for (i = 0; ctx->cmds[i].command; i++)
+            mm_base_modem_at_command_alloc_clear (&ctx->cmds[i]);
+        g_free (ctx->cmds);
+    }
+    g_slice_free (SetCurrentBandsContext, ctx);
+}
+
 static gboolean
 set_current_bands_finish (MMIfaceModem  *self,
                           GAsyncResult  *res,
@@ -2093,23 +2107,17 @@
 }
 
 static void
-scfg_set_ready_sequence (MMBaseModem  *_self,
+scfg_set_ready_sequence (MMBaseModem  *self,
                          GAsyncResult *res,
                          GTask        *task)
 {
     GError *error = NULL;
-    gpointer ctx = NULL;
-    guint i;
-    MMBroadbandModemCinterion *self;
 
-    self = g_task_get_source_object (task);
-    for (i = 0; self->priv->cmds[i].command; i++)
-        mm_base_modem_at_command_alloc_clear (&self->priv->cmds[i]);
-    g_free (self->priv->cmds);
-    self->priv->cmds = NULL;
-
-    mm_base_modem_at_sequence_finish (_self, res, &ctx, &error);
-    g_task_return_boolean (task, TRUE);
+    mm_base_modem_at_sequence_finish (self, res, NULL, &error);
+    if (error)
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
 
@@ -2154,39 +2162,83 @@
                                   FALSE,
                                   (GAsyncReadyCallback)scfg_set_ready,
                                   task);
-    } else { /* self->priv->rb_format == MM_CINTERION_RADIO_BAND_FORMAT_MULTIPLE */
+        return;
+    }
+
+    if (self->priv->rb_format == MM_CINTERION_RADIO_BAND_FORMAT_MULTIPLE) {
+        SetCurrentBandsContext *ctx;
+
+        ctx = g_slice_new0 (SetCurrentBandsContext);
+        g_task_set_task_data (task, ctx, (GDestroyNotify)set_current_bands_context_free);
+
         if (self->priv->modem_family == MM_CINTERION_MODEM_FAMILY_IMT) {
             g_autofree gchar *bandstr2G = NULL;
             g_autofree gchar *bandstr3G = NULL;
             g_autofree gchar *bandstr4G = NULL;
+            g_autofree gchar *bandstr2G_enc = NULL;
+            g_autofree gchar *bandstr3G_enc = NULL;
+            g_autofree gchar *bandstr4G_enc = NULL;
 
             bandstr2G = g_strdup_printf ("0x%08X", band[MM_CINTERION_RB_BLOCK_GSM]);
             bandstr3G = g_strdup_printf ("0x%08X", band[MM_CINTERION_RB_BLOCK_UMTS]);
             bandstr4G = g_strdup_printf ("0x%08X", band[MM_CINTERION_RB_BLOCK_LTE_LOW]);
-            bandstr2G = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (self), bandstr2G);
-            bandstr3G = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (self), bandstr3G);
-            bandstr4G = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (self), bandstr4G);
-            self->priv->cmds = g_new0 (MMBaseModemAtCommandAlloc, 3 + 1);
-            self->priv->cmds[0].command = g_strdup_printf ("^SCFG=\"Radio/Band/2G\",\"%s\"", bandstr2G);
-            self->priv->cmds[1].command = g_strdup_printf ("^SCFG=\"Radio/Band/3G\",\"%s\"", bandstr3G);
-            self->priv->cmds[2].command = g_strdup_printf ("^SCFG=\"Radio/Band/4G\",\"%s\"", bandstr4G);
-            self->priv->cmds[0].timeout = self->priv->cmds[1].timeout = self->priv->cmds[2].timeout = 60;
+
+            bandstr2G_enc = mm_modem_charset_str_from_utf8 (bandstr2G,
+                                                            mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (self)),
+                                                            FALSE,
+                                                            &error);
+            if (!bandstr2G_enc) {
+                g_prefix_error (&error, "Couldn't convert 2G band string to current charset: ");
+                g_task_return_error (task, error);
+                g_object_unref (task);
+                return;
+            }
+
+            bandstr3G_enc = mm_modem_charset_str_from_utf8 (bandstr3G,
+                                                            mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (self)),
+                                                            FALSE,
+                                                            &error);
+            if (!bandstr3G_enc) {
+                g_prefix_error (&error, "Couldn't convert 3G band string to current charset: ");
+                g_task_return_error (task, error);
+                g_object_unref (task);
+                return;
+            }
+
+            bandstr4G_enc = mm_modem_charset_str_from_utf8 (bandstr4G,
+                                                            mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (self)),
+                                                            FALSE,
+                                                            &error);
+            if (!bandstr4G_enc) {
+                g_prefix_error (&error, "Couldn't convert 4G band string to current charset: ");
+                g_task_return_error (task, error);
+                g_object_unref (task);
+                return;
+            }
+
+            ctx->cmds = g_new0 (MMBaseModemAtCommandAlloc, 3 + 1);
+            ctx->cmds[0].command = g_strdup_printf ("^SCFG=\"Radio/Band/2G\",\"%s\"", bandstr2G_enc);
+            ctx->cmds[1].command = g_strdup_printf ("^SCFG=\"Radio/Band/3G\",\"%s\"", bandstr3G_enc);
+            ctx->cmds[2].command = g_strdup_printf ("^SCFG=\"Radio/Band/4G\",\"%s\"", bandstr4G_enc);
+            ctx->cmds[0].timeout = ctx->cmds[1].timeout = ctx->cmds[2].timeout = 60;
         } else {
-            self->priv->cmds = g_new0 (MMBaseModemAtCommandAlloc, 3 + 1);
-            self->priv->cmds[0].command = g_strdup_printf ("^SCFG=\"Radio/Band/2G\",\"%08x\",,1", band[MM_CINTERION_RB_BLOCK_GSM]);
-            self->priv->cmds[1].command = g_strdup_printf ("^SCFG=\"Radio/Band/3G\",\"%08x\",,1", band[MM_CINTERION_RB_BLOCK_UMTS]);
-            self->priv->cmds[2].command = g_strdup_printf ("^SCFG=\"Radio/Band/4G\",\"%08x\",\"%08x\",1", band[MM_CINTERION_RB_BLOCK_LTE_LOW], band[MM_CINTERION_RB_BLOCK_LTE_HIGH]);
-            self->priv->cmds[0].timeout = self->priv->cmds[1].timeout = self->priv->cmds[2].timeout = 15;
+            ctx->cmds = g_new0 (MMBaseModemAtCommandAlloc, 3 + 1);
+            ctx->cmds[0].command = g_strdup_printf ("^SCFG=\"Radio/Band/2G\",\"%08x\",,1", band[MM_CINTERION_RB_BLOCK_GSM]);
+            ctx->cmds[1].command = g_strdup_printf ("^SCFG=\"Radio/Band/3G\",\"%08x\",,1", band[MM_CINTERION_RB_BLOCK_UMTS]);
+            ctx->cmds[2].command = g_strdup_printf ("^SCFG=\"Radio/Band/4G\",\"%08x\",\"%08x\",1", band[MM_CINTERION_RB_BLOCK_LTE_LOW], band[MM_CINTERION_RB_BLOCK_LTE_HIGH]);
+            ctx->cmds[0].timeout = ctx->cmds[1].timeout = ctx->cmds[2].timeout = 15;
         }
 
         mm_base_modem_at_sequence (MM_BASE_MODEM (self),
-                                   (const MMBaseModemAtCommand *)self->priv->cmds,
+                                   (const MMBaseModemAtCommand *)ctx->cmds,
                                    NULL,
                                    NULL,
                                    (GAsyncReadyCallback)scfg_set_ready_sequence,
                                    task);
+        return;
     }
 
+    g_assert_not_reached ();
 }
 
 static void
@@ -2196,8 +2248,9 @@
     MMBroadbandModemCinterion *self;
     GError                    *error = NULL;
     guint                      band[MM_CINTERION_RB_BLOCK_N] = { 0 };
-    gchar                     *cmd;
-    gchar                     *bandstr;
+    g_autofree gchar          *cmd = NULL;
+    g_autofree gchar          *bandstr = NULL;
+    g_autofree gchar          *bandstr_enc = NULL;
 
     self = g_task_get_source_object (task);
 
@@ -2215,12 +2268,13 @@
 
     /* Build string with the value, in the proper charset */
     bandstr = g_strdup_printf ("%u", band[MM_CINTERION_RB_BLOCK_LEGACY]);
-    bandstr = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (self), bandstr);
-    if (!bandstr) {
-        g_task_return_new_error (task,
-                                 MM_CORE_ERROR,
-                                 MM_CORE_ERROR_UNSUPPORTED,
-                                 "Couldn't convert band set to current charset");
+    bandstr_enc = mm_modem_charset_str_from_utf8 (bandstr,
+                                                  mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (self)),
+                                                  FALSE,
+                                                  &error);
+    if (!bandstr_enc) {
+        g_prefix_error (&error, "Couldn't convert band string to current charset: ");
+        g_task_return_error (task, error);
         g_object_unref (task);
         return;
     }
@@ -2231,17 +2285,13 @@
      * the modem to connect at that specific frequency only. Note that we will be
      * passing double-quote enclosed strings here!
      */
-    cmd = g_strdup_printf ("^SCFG=\"Radio/Band\",\"%s\",\"%s\"", bandstr, bandstr);
-
+    cmd = g_strdup_printf ("^SCFG=\"Radio/Band\",\"%s\",\"%s\"", bandstr_enc, bandstr_enc);
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               cmd,
                               15,
                               FALSE,
                               (GAsyncReadyCallback)scfg_set_ready,
                               task);
-
-    g_free (cmd);
-    g_free (bandstr);
 }
 
 static void
diff --git a/plugins/cinterion/mm-modem-helpers-cinterion.c b/plugins/cinterion/mm-modem-helpers-cinterion.c
index 0522881..cf18ea6 100644
--- a/plugins/cinterion/mm-modem-helpers-cinterion.c
+++ b/plugins/cinterion/mm-modem-helpers-cinterion.c
@@ -217,20 +217,34 @@
 }
 
 static guint
-take_and_convert_from_matched_string (gchar                  *str,
-                                      MMModemCharset          charset,
-                                      MMCinterionModemFamily  modem_family)
+take_and_convert_from_matched_string (gchar                   *str,
+                                      MMModemCharset           charset,
+                                      MMCinterionModemFamily   modem_family,
+                                      GError                 **error)
 {
-    guint val = 0;
+    guint             val = 0;
+    g_autofree gchar *utf8 = NULL;
+    g_autofree gchar *taken_str = str;
 
-    if (!str)
+    if (!taken_str) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Couldn't convert to integer number: no input string");
         return 0;
+    }
 
-    if (modem_family == MM_CINTERION_MODEM_FAMILY_IMT)
-        str = mm_charset_take_and_convert_to_utf8 (str, charset);
+    if (modem_family == MM_CINTERION_MODEM_FAMILY_IMT) {
+        utf8 = mm_modem_charset_str_to_utf8 (taken_str, -1, charset, FALSE, error);
+        if (!utf8) {
+            g_prefix_error (error, "Couldn't convert to integer number: ");
+            return 0;
+        }
+    }
 
-    mm_get_uint_from_hex_str (str, &val);
-    g_free (str);
+    if (!mm_get_uint_from_hex_str (utf8 ? utf8 : taken_str, &val)) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Couldn't convert to integer number: wrong hex encoding: %s", utf8 ? utf8 : taken_str);
+        return 0;
+    }
 
     return val;
 }
@@ -292,12 +306,16 @@
         goto finish;
     }
 
-    r2 = g_regex_new ("\\^SCFG:\\s*\"Radio/Band/([234]G)\",\\(\"?([0-9A-Fa-fx]*)\"?-\"?([0-9A-Fa-fx]*)\"?\\)(,*\\(\"?([0-9A-Fa-fx]*)\"?-\"?([0-9A-Fa-fx]*)\"?\\))?",
+    r2 = g_regex_new ("\\^SCFG:\\s*\"Radio/Band/([234]G)\","
+                      "\\(\"?([0-9A-Fa-fx]*)\"?-\"?([0-9A-Fa-fx]*)\"?\\)"
+                      "(,*\\(\"?([0-9A-Fa-fx]*)\"?-\"?([0-9A-Fa-fx]*)\"?\\))?",
                      0, 0, NULL);
     g_assert (r2 != NULL);
+
     g_regex_match_full (r2, response, strlen (response), 0, 0, &match_info2, &inner_error);
     if (inner_error)
         goto finish;
+
     while (g_match_info_matches (match_info2)) {
         g_autofree gchar *techstr = NULL;
         guint             maxband;
@@ -306,16 +324,28 @@
 
         techstr = mm_get_string_unquoted_from_match_info (match_info2, 1);
         if (g_strcmp0 (techstr, "2G") == 0) {
-            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3), charset, modem_family);
+            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3),
+                                                            charset, modem_family, &inner_error);
+            if (inner_error)
+                break;
             parse_bands (maxband, &bands, MM_CINTERION_RB_BLOCK_GSM, modem_family);
         } else if (g_strcmp0 (techstr, "3G") == 0) {
-            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3), charset, modem_family);
+            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3),
+                                                            charset, modem_family, &inner_error);
+            if (inner_error)
+                break;
             parse_bands (maxband, &bands, MM_CINTERION_RB_BLOCK_UMTS, modem_family);
         } else if (g_strcmp0 (techstr, "4G") == 0) {
-            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3), charset, modem_family);
+            maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 3),
+                                                            charset, modem_family, &inner_error);
+            if (inner_error)
+                break;
             parse_bands (maxband, &bands, MM_CINTERION_RB_BLOCK_LTE_LOW, modem_family);
             if (modem_family == MM_CINTERION_MODEM_FAMILY_DEFAULT) {
-                maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 6), charset, modem_family);
+                maxband = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info2, 6),
+                                                                charset, modem_family, &inner_error);
+                if (inner_error)
+                    break;
                 parse_bands (maxband, &bands, MM_CINTERION_RB_BLOCK_LTE_HIGH, modem_family);
             }
         } else {
@@ -407,9 +437,11 @@
     if (format == MM_CINTERION_RADIO_BAND_FORMAT_SINGLE) {
         r = g_regex_new ("\\^SCFG:\\s*\"Radio/Band\",\\s*\"?([0-9a-fA-F]*)\"?", 0, 0, NULL);
         g_assert (r != NULL);
+
         g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
         if (inner_error)
             goto finish;
+
         if (g_match_info_matches (match_info)) {
             g_autofree gchar *currentstr = NULL;
             guint current = 0;
@@ -438,26 +470,40 @@
         r = g_regex_new ("\\^SCFG:\\s*\"Radio/Band/([234]G)\",\"?([0-9A-Fa-fx]*)\"?,?\"?([0-9A-Fa-fx]*)?\"?",
                          0, 0, NULL);
         g_assert (r != NULL);
+
         g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
         if (inner_error)
             goto finish;
+
         while (g_match_info_matches (match_info)) {
             g_autofree gchar *techstr = NULL;
             guint current;
 
             techstr = mm_get_string_unquoted_from_match_info (match_info, 1);
-            if (g_strcmp0(techstr, "2G") == 0) {
-                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2), charset, modem_family);
+            if (g_strcmp0 (techstr, "2G") == 0) {
+                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2),
+                                                                charset, modem_family, &inner_error);
+                if (inner_error)
+                    break;
                 parse_bands (current, &bands, MM_CINTERION_RB_BLOCK_GSM, modem_family);
 
-            } else if (g_strcmp0(techstr, "3G") == 0) {
-                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2), charset, modem_family);
+            } else if (g_strcmp0 (techstr, "3G") == 0) {
+                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2),
+                                                                charset, modem_family, &inner_error);
+                if (inner_error)
+                    break;
                 parse_bands (current, &bands, MM_CINTERION_RB_BLOCK_UMTS, modem_family);
-            } else if (g_strcmp0(techstr, "4G") == 0) {
-                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2), charset, modem_family);
+            } else if (g_strcmp0 (techstr, "4G") == 0) {
+                current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 2),
+                                                                charset, modem_family, &inner_error);
+                if (inner_error)
+                    break;
                 parse_bands (current, &bands, MM_CINTERION_RB_BLOCK_LTE_LOW, modem_family);
                 if (modem_family == MM_CINTERION_MODEM_FAMILY_DEFAULT) {
-                    current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 3), charset, modem_family);
+                    current = take_and_convert_from_matched_string (mm_get_string_unquoted_from_match_info (match_info, 3),
+                                                                    charset, modem_family, &inner_error);
+                    if (inner_error)
+                        break;
                     parse_bands (current, &bands, MM_CINTERION_RB_BLOCK_LTE_HIGH, modem_family);
                 }
             } else {
@@ -1477,9 +1523,15 @@
 
     mno = mm_get_string_unquoted_from_match_info (match_info, 1);
     if (mno && modem_family == MM_CINTERION_MODEM_FAMILY_IMT) {
-        mno = mm_charset_take_and_convert_to_utf8 (mno, charset);
-        mm_obj_dbg (log_object, "current mno: %s", mno);
+        gchar *mno_utf8;
+
+        mno_utf8 = mm_modem_charset_str_to_utf8 (mno, -1, charset, FALSE, error);
+        if (!mno_utf8)
+            return FALSE;
+        g_free (mno);
+        mno = mno_utf8;
     }
+    mm_obj_dbg (log_object, "current mno: %s", mno ? mno : "none");
 
     /* for Cinterion LTE modules, some CID numbers have special meaning.
      * This is dictated by the chipset and by the MNO:
diff --git a/plugins/huawei/mm-broadband-modem-huawei.c b/plugins/huawei/mm-broadband-modem-huawei.c
index 19249a8..2c341d3 100644
--- a/plugins/huawei/mm-broadband-modem-huawei.c
+++ b/plugins/huawei/mm-broadband-modem-huawei.c
@@ -177,8 +177,7 @@
     /* Additional cdc-wdm ports used for dialing */
     cdc_wdm_at_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                                  MM_PORT_SUBSYS_USBMISC,
-                                                 MM_PORT_TYPE_AT,
-                                                 NULL);
+                                                 MM_PORT_TYPE_AT);
 
     return g_list_concat (out, cdc_wdm_at_ports);
 }
@@ -2201,8 +2200,7 @@
     /* Find the CDC-WDM port on the same USB interface as the given net port */
     cdc_wdm_at_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                                  MM_PORT_SUBSYS_USBMISC,
-                                                 MM_PORT_TYPE_AT,
-                                                 NULL);
+                                                 MM_PORT_TYPE_AT);
     for (l = cdc_wdm_at_ports; l && !found; l = g_list_next (l)) {
         const gchar  *wdm_port_parent_path;
 
@@ -2301,28 +2299,27 @@
         guint *scheme,
         GError **error)
 {
-    gchar *hex;
-    guint8 *gsm, *packed;
-    guint32 len = 0, packed_len = 0;
+    g_autoptr(GByteArray)  gsm = NULL;
+    g_autofree guint8     *packed = NULL;
+    guint32                packed_len = 0;
+
+    gsm = mm_modem_charset_bytearray_from_utf8 (command, MM_MODEM_CHARSET_GSM, FALSE, error);
+    if (!gsm)
+        return NULL;
 
     *scheme = MM_MODEM_GSM_USSD_SCHEME_7BIT;
-    gsm = mm_charset_utf8_to_unpacked_gsm (command, &len);
 
     /* If command is a multiple of 7 characters long, Huawei firmwares
      * apparently want that padded.  Maybe all modems?
      */
-    if (len % 7 == 0) {
-        gsm = g_realloc (gsm, len + 1);
-        gsm[len] = 0x0d;
-        len++;
+    if (gsm->len % 7 == 0) {
+        static const guint8 padding = 0x0d;
+
+        g_byte_array_append (gsm, &padding, 1);
     }
 
-    packed = mm_charset_gsm_pack (gsm, len, 0, &packed_len);
-    hex = mm_utils_bin2hexstr (packed, packed_len);
-    g_free (packed);
-    g_free (gsm);
-
-    return hex;
+    packed = mm_charset_gsm_pack (gsm->data, gsm->len, 0, &packed_len);
+    return mm_utils_bin2hexstr (packed, packed_len);
 }
 
 static gchar *
@@ -2330,21 +2327,25 @@
         const gchar *reply,
         GError **error)
 {
-    gchar *bin, *utf8;
-    guint8 *unpacked;
-    gsize bin_len;
-    guint32 unpacked_len;
+    g_autofree guint8    *bin = NULL;
+    gsize                 bin_len = 0;
+    g_autofree guint8    *unpacked = NULL;
+    guint32               unpacked_len;
+    g_autoptr(GByteArray) unpacked_array = NULL;
 
-    bin = mm_utils_hexstr2bin (reply, &bin_len);
-    unpacked = mm_charset_gsm_unpack ((guint8*) bin, (bin_len * 8) / 7, 0, &unpacked_len);
+    bin = mm_utils_hexstr2bin (reply, -1, &bin_len, error);
+    if (!bin)
+        return NULL;
+
+    unpacked = mm_charset_gsm_unpack (bin, (bin_len * 8) / 7, 0, &unpacked_len);
     /* if the last character in a 7-byte block is padding, then drop it */
     if ((bin_len % 7 == 0) && (unpacked[unpacked_len - 1] == 0x0d))
         unpacked_len--;
-    utf8 = (char*) mm_charset_gsm_unpacked_to_utf8 (unpacked, unpacked_len);
 
-    g_free (bin);
-    g_free (unpacked);
-    return utf8;
+    unpacked_array = g_byte_array_sized_new (unpacked_len);
+    g_byte_array_append (unpacked_array, unpacked, unpacked_len);
+
+    return mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error);
 }
 
 /*****************************************************************************/
diff --git a/plugins/huawei/mm-modem-helpers-huawei.c b/plugins/huawei/mm-modem-helpers-huawei.c
index 2cd94e6..1b44305 100644
--- a/plugins/huawei/mm-modem-helpers-huawei.c
+++ b/plugins/huawei/mm-modem-helpers-huawei.c
@@ -157,14 +157,15 @@
 
 static gboolean
 match_info_to_ip4_addr (GMatchInfo *match_info,
-                        guint match_index,
-                        guint *out_addr)
+                        guint       match_index,
+                        guint      *out_addr)
 {
-    gchar *s, *bin = NULL;
-    gchar buf[9];
-    gsize len, bin_len;
-    gboolean success = FALSE;
-    guint32 aux;
+    g_autofree gchar  *s = NULL;
+    g_autofree guint8 *bin = NULL;
+    gchar              buf[9];
+    gsize              len;
+    gsize              bin_len;
+    guint32            aux;
 
     s = g_match_info_fetch (match_info, match_index);
     g_return_val_if_fail (s != NULL, FALSE);
@@ -172,11 +173,11 @@
     len = strlen (s);
     if (len == 1 && s[0] == '0') {
         *out_addr = 0;
-        success = TRUE;
-        goto done;
+        return TRUE;
     }
+
     if (len < 7 || len > 8)
-        goto done;
+        return FALSE;
 
     /* Handle possibly missing leading zero */
     memset (buf, 0, sizeof (buf));
@@ -188,18 +189,13 @@
     else
         g_assert_not_reached ();
 
-    bin = mm_utils_hexstr2bin (buf, &bin_len);
+    bin = mm_utils_hexstr2bin (buf, -1, &bin_len, NULL);
     if (!bin || bin_len != 4)
-        goto done;
+        return FALSE;
 
     memcpy (&aux, bin, 4);
     *out_addr = GUINT32_SWAP_LE_BE (aux);
-    success = TRUE;
-
-done:
-    g_free (s);
-    g_free (bin);
-    return success;
+    return TRUE;
 }
 
 gboolean
diff --git a/plugins/huawei/mm-plugin-huawei.c b/plugins/huawei/mm-plugin-huawei.c
index 4c5c39e..07652ec 100644
--- a/plugins/huawei/mm-plugin-huawei.c
+++ b/plugins/huawei/mm-plugin-huawei.c
@@ -56,7 +56,7 @@
 
 typedef struct {
     MMPortProbe *probe;
-    guint        first_usbif;
+    gint         first_usbif;
     guint        timeout_id;
     gboolean     custom_init_run;
 } FirstInterfaceContext;
@@ -177,23 +177,23 @@
                 MMDevice    *device)
 {
     FirstInterfaceContext *fi_ctx;
-    GList *l;
-    guint closest;
+    GList                 *l;
+    gint                   closest;
 
     fi_ctx = g_object_get_data (G_OBJECT (device), TAG_FIRST_INTERFACE_CONTEXT);
     g_assert (fi_ctx != NULL);
 
     /* Look for the next closest one among the list of interfaces in the device,
      * and enable that one as being first */
-    closest = G_MAXUINT;
+    closest = G_MAXINT;
     for (l = mm_device_peek_port_probe_list (device); l; l = g_list_next (l)) {
         MMPortProbe *iter = MM_PORT_PROBE (l->data);
 
         /* Only expect ttys for next probing attempt */
         if (g_str_equal (mm_port_probe_get_port_subsys (iter), "tty")) {
-            guint usbif;
+            gint usbif;
 
-            usbif = mm_kernel_device_get_property_as_int_hex (mm_port_probe_peek_port (iter), "ID_USB_INTERFACE_NUM");
+            usbif = mm_kernel_device_get_interface_number (mm_port_probe_peek_port (iter));
             if (usbif == fi_ctx->first_usbif) {
                 /* This is the one we just probed, which wasn't yet removed, so just skip it */
             } else if (usbif > fi_ctx->first_usbif &&
@@ -203,7 +203,7 @@
         }
     }
 
-    if (closest == G_MAXUINT) {
+    if (closest == G_MAXINT) {
         /* No more ttys to try! Just return something */
         closest = 0;
         mm_obj_dbg (probe, "no more ports to run initial probing");
@@ -359,9 +359,7 @@
     g_task_set_task_data (task, ctx, (GDestroyNotify)huawei_custom_init_context_free);
 
     /* Custom init only to be run in the first interface */
-    if (mm_kernel_device_get_property_as_int_hex (mm_port_probe_peek_port (probe),
-                                                  "ID_USB_INTERFACE_NUM") != fi_ctx->first_usbif) {
-
+    if (mm_kernel_device_get_interface_number (mm_port_probe_peek_port (probe)) != fi_ctx->first_usbif) {
         if (fi_ctx->custom_init_run)
             /* If custom init was run already, we can consider this as successfully run */
             g_task_return_boolean (task, TRUE);
@@ -391,8 +389,8 @@
 probe_cmp_by_usbif (MMPortProbe *a,
                     MMPortProbe *b)
 {
-    return ((gint) mm_kernel_device_get_property_as_int_hex (mm_port_probe_peek_port (a), "ID_USB_INTERFACE_NUM") -
-            (gint) mm_kernel_device_get_property_as_int_hex (mm_port_probe_peek_port (b), "ID_USB_INTERFACE_NUM"));
+    return (mm_kernel_device_get_interface_number (mm_port_probe_peek_port (a)) -
+            mm_kernel_device_get_interface_number (mm_port_probe_peek_port (b)));
 }
 
 static guint
diff --git a/plugins/mbm/mm-broadband-bearer-mbm.c b/plugins/mbm/mm-broadband-bearer-mbm.c
index b4bba2d..8de7a09 100644
--- a/plugins/mbm/mm-broadband-bearer-mbm.c
+++ b/plugins/mbm/mm-broadband-bearer-mbm.c
@@ -359,22 +359,35 @@
 
     /* Both user and password are required; otherwise firmware returns an error */
     if (user || password) {
-        gchar *command;
-        gchar *encoded_user;
-        gchar *encoded_password;
+        g_autofree gchar  *command = NULL;
+        g_autofree gchar  *user_enc = NULL;
+        g_autofree gchar  *password_enc = NULL;
+        GError            *error = NULL;
 
-        encoded_user = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (ctx->modem),
-                                                                               g_strdup (user));
-        encoded_password = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (ctx->modem),
-                                                                                   g_strdup (password));
+        user_enc = mm_modem_charset_str_from_utf8 (user,
+                                                   mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (ctx->modem)),
+                                                   FALSE,
+                                                   &error);
+        if (!user_enc) {
+            g_prefix_error (&error, "Couldn't convert user to current charset: ");
+            g_task_return_error (task, error);
+            g_object_unref (task);
+            return;
+        }
+
+        password_enc = mm_modem_charset_str_from_utf8 (password,
+                                                       mm_broadband_modem_get_current_charset (MM_BROADBAND_MODEM (ctx->modem)),
+                                                       FALSE,
+                                                       &error);
+        if (!password_enc) {
+            g_prefix_error (&error, "Couldn't convert password to current charset: ");
+            g_task_return_error (task, error);
+            g_object_unref (task);
+            return;
+        }
 
         command = g_strdup_printf ("AT*EIAAUW=%d,1,\"%s\",\"%s\"",
-                                   ctx->cid,
-                                   encoded_user ? encoded_user : "",
-                                   encoded_password ? encoded_password : "");
-        g_free (encoded_user);
-        g_free (encoded_password);
-
+                                   ctx->cid, user_enc, password_enc);
         mm_base_modem_at_command_full (ctx->modem,
                                        ctx->primary,
                                        command,
@@ -384,7 +397,6 @@
                                        g_task_get_cancellable (task),
                                        (GAsyncReadyCallback) authenticate_ready,
                                        task);
-        g_free (command);
         return;
     }
 
diff --git a/plugins/option/mm-plugin-option.c b/plugins/option/mm-plugin-option.c
index 3ba183e..1f72325 100644
--- a/plugins/option/mm-plugin-option.c
+++ b/plugins/option/mm-plugin-option.c
@@ -56,7 +56,7 @@
 {
     MMPortSerialAtFlag pflags = MM_PORT_SERIAL_AT_FLAG_NONE;
     MMKernelDevice *port;
-    guint usbif;
+    gint usbif;
 
     /* The Option plugin cannot do anything with non-AT ports */
     if (!mm_port_probe_is_at (probe)) {
@@ -73,7 +73,7 @@
      * the modem/data port, per mail with Option engineers.  Only this port
      * will emit responses to dialing commands.
      */
-    usbif = mm_kernel_device_get_property_as_int_hex (port, "ID_USB_INTERFACE_NUM");
+    usbif = mm_kernel_device_get_interface_number (port);
     if (usbif == 0)
         pflags = MM_PORT_SERIAL_AT_FLAG_PRIMARY | MM_PORT_SERIAL_AT_FLAG_PPP;
 
diff --git a/plugins/qcom-soc/mm-broadband-modem-qmi-qcom-soc.c b/plugins/qcom-soc/mm-broadband-modem-qmi-qcom-soc.c
index 58a6e5e..0256903 100644
--- a/plugins/qcom-soc/mm-broadband-modem-qmi-qcom-soc.c
+++ b/plugins/qcom-soc/mm-broadband-modem-qmi-qcom-soc.c
@@ -81,8 +81,7 @@
     /* Find one QMI port, we don't care which one */
     rpmsg_qmi_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                                 MM_PORT_SUBSYS_RPMSG,
-                                                MM_PORT_TYPE_QMI,
-                                                NULL);
+                                                MM_PORT_TYPE_QMI);
     if (!rpmsg_qmi_ports) {
         g_set_error (error,
                      MM_CORE_ERROR,
@@ -93,7 +92,8 @@
     }
 
     /* Set outputs */
-    *out_sio_port = sio_port_per_port_number[net_port_number];
+    if (out_sio_port)
+        *out_sio_port = sio_port_per_port_number[net_port_number];
     found = MM_PORT_QMI (rpmsg_qmi_ports->data);
 
     g_list_free_full (rpmsg_qmi_ports, g_object_unref);
diff --git a/plugins/telit/mm-common-telit.c b/plugins/telit/mm-common-telit.c
index f53f5b75..2e0b380 100644
--- a/plugins/telit/mm-common-telit.c
+++ b/plugins/telit/mm-common-telit.c
@@ -59,7 +59,7 @@
     if (g_object_get_data (G_OBJECT (device), TAG_GETPORTCFG_SUPPORTED) != NULL) {
         guint usbif;
 
-        usbif = mm_kernel_device_get_property_as_int_hex (port, "ID_USB_INTERFACE_NUM");
+        usbif = (guint) mm_kernel_device_get_interface_number (port);
         if (usbif == GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (device), TAG_TELIT_MODEM_PORT))) {
             mm_obj_dbg (self, "AT port '%s/%s' flagged as primary",
                         mm_port_probe_get_port_subsys (probe),
@@ -257,9 +257,7 @@
      * is always linked to an AT port
      */
     port = mm_port_probe_peek_port (probe);
-    if (!ctx->getportcfg_done &&
-        g_strcmp0 (mm_kernel_device_get_property (port, "ID_USB_INTERFACE_NUM"), "00") == 0) {
-
+    if (!ctx->getportcfg_done && mm_kernel_device_get_interface_number (port) == 0) {
         if (ctx->getportcfg_retries == 0)
             goto out;
         ctx->getportcfg_retries--;
diff --git a/src/Makefile.am b/src/Makefile.am
index e80c10a..05fc6d4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -287,7 +287,7 @@
 	main.c \
 	mm-context.h \
 	mm-context.c \
-	mm-utils.h \
+	mm-utils.h mm-utils.c \
 	mm-private-boxed-types.h \
 	mm-private-boxed-types.c \
 	mm-auth-provider.h \
diff --git a/src/kerneldevice/mm-kernel-device-generic.c b/src/kerneldevice/mm-kernel-device-generic.c
index b1a3bf9..3428563 100644
--- a/src/kerneldevice/mm-kernel-device-generic.c
+++ b/src/kerneldevice/mm-kernel-device-generic.c
@@ -27,6 +27,7 @@
 #include "mm-kernel-device-generic.h"
 #include "mm-kernel-device-generic-rules.h"
 #include "mm-log-object.h"
+#include "mm-utils.h"
 
 #if !defined UDEVRULESDIR
 # error UDEVRULESDIR is not defined
@@ -551,6 +552,12 @@
 }
 
 static gint
+kernel_device_get_interface_number (MMKernelDevice *self)
+{
+    return (gint) MM_KERNEL_DEVICE_GENERIC (self)->priv->interface_number;
+}
+
+static gint
 kernel_device_get_interface_class (MMKernelDevice *self)
 {
     return (gint) MM_KERNEL_DEVICE_GENERIC (self)->priv->interface_class;
@@ -1172,6 +1179,7 @@
     kernel_device_class->get_physdev_subsystem     = kernel_device_get_physdev_subsystem;
     kernel_device_class->get_physdev_manufacturer  = kernel_device_get_physdev_manufacturer;
     kernel_device_class->get_physdev_product       = kernel_device_get_physdev_product;
+    kernel_device_class->get_interface_number      = kernel_device_get_interface_number;
     kernel_device_class->get_interface_class       = kernel_device_get_interface_class;
     kernel_device_class->get_interface_subclass    = kernel_device_get_interface_subclass;
     kernel_device_class->get_interface_protocol    = kernel_device_get_interface_protocol;
diff --git a/src/kerneldevice/mm-kernel-device-udev.c b/src/kerneldevice/mm-kernel-device-udev.c
index f75104b..0687ec6 100644
--- a/src/kerneldevice/mm-kernel-device-udev.c
+++ b/src/kerneldevice/mm-kernel-device-udev.c
@@ -31,6 +31,7 @@
 
 enum {
     PROP_0,
+    PROP_UDEV_CLIENT,
     PROP_UDEV_DEVICE,
     PROP_PROPERTIES,
     PROP_LAST
@@ -39,6 +40,7 @@
 static GParamSpec *properties[PROP_LAST];
 
 struct _MMKernelDeviceUdevPrivate {
+    GUdevClient *client;
     GUdevDevice *device;
 
     GUdevDevice *interface;
@@ -403,6 +405,15 @@
 }
 
 static gint
+kernel_device_get_interface_number (MMKernelDevice *_self)
+{
+    MMKernelDeviceUdev *self;
+
+    self = MM_KERNEL_DEVICE_UDEV (_self);
+    return (self->priv->interface ? (gint) udev_device_get_sysfs_attr_as_hex (self->priv->interface, "bInterfaceNumber") : -1);
+}
+
+static gint
 kernel_device_get_interface_class (MMKernelDevice *_self)
 {
     MMKernelDeviceUdev *self;
@@ -556,7 +567,8 @@
 /*****************************************************************************/
 
 MMKernelDevice *
-mm_kernel_device_udev_new (GUdevDevice *udev_device)
+mm_kernel_device_udev_new (GUdevClient *udev_client,
+                           GUdevDevice *udev_device)
 {
     GError         *error = NULL;
     MMKernelDevice *self;
@@ -565,6 +577,7 @@
     self = MM_KERNEL_DEVICE (g_initable_new (MM_TYPE_KERNEL_DEVICE_UDEV,
                                              NULL,
                                              &error,
+                                             "udev-client", udev_client,
                                              "udev-device", udev_device,
                                              NULL));
     g_assert_no_error (error);
@@ -574,13 +587,15 @@
 /*****************************************************************************/
 
 MMKernelDevice *
-mm_kernel_device_udev_new_from_properties (MMKernelEventProperties  *props,
+mm_kernel_device_udev_new_from_properties (GUdevClient              *udev_client,
+                                           MMKernelEventProperties  *props,
                                            GError                  **error)
 {
     return MM_KERNEL_DEVICE (g_initable_new (MM_TYPE_KERNEL_DEVICE_UDEV,
                                              NULL,
                                              error,
-                                             "properties", props,
+                                             "udev-client", udev_client,
+                                             "properties",  props,
                                              NULL));
 }
 
@@ -602,6 +617,10 @@
     MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (object);
 
     switch (prop_id) {
+    case PROP_UDEV_CLIENT:
+        g_assert (!self->priv->client);
+        self->priv->client = g_value_dup_object (value);
+        break;
     case PROP_UDEV_DEVICE:
         g_assert (!self->priv->device);
         self->priv->device = g_value_dup_object (value);
@@ -625,6 +644,9 @@
     MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (object);
 
     switch (prop_id) {
+    case PROP_UDEV_CLIENT:
+        g_value_set_object (value, self->priv->client);
+        break;
     case PROP_UDEV_DEVICE:
         g_value_set_object (value, self->priv->device);
         break;
@@ -646,6 +668,12 @@
     const gchar        *subsystem;
     const gchar        *name;
 
+    if (!self->priv->client) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "missing client in kernel device");
+        return FALSE;
+    }
+
     /* When created from a GUdevDevice, we're done */
     if (self->priv->device) {
         preload_contents (self);
@@ -675,23 +703,15 @@
 
     /* On remove events, we don't look for the GUdevDevice */
     if (g_strcmp0 (mm_kernel_event_properties_get_action (self->priv->properties), "remove")) {
-        GUdevClient *client;
-        GUdevDevice *device;
-
-        client = g_udev_client_new (NULL);
-        device = g_udev_client_query_by_subsystem_and_name (client, subsystem, name);
-        if (!device) {
+        g_assert (!self->priv->device);
+        self->priv->device = g_udev_client_query_by_subsystem_and_name (self->priv->client, subsystem, name);
+        if (!self->priv->device) {
             g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                          "device %s/%s not found",
                          subsystem,
                          name);
-            g_object_unref (client);
             return FALSE;
         }
-
-        /* Store device */
-        self->priv->device = device;
-        g_object_unref (client);
     }
 
     if (self->priv->device)
@@ -708,6 +728,7 @@
     g_clear_object  (&self->priv->physdev);
     g_clear_object  (&self->priv->interface);
     g_clear_object  (&self->priv->device);
+    g_clear_object  (&self->priv->client);
     g_clear_object  (&self->priv->properties);
 
     G_OBJECT_CLASS (mm_kernel_device_udev_parent_class)->dispose (object);
@@ -743,6 +764,7 @@
     kernel_device_class->get_physdev_subsystem     = kernel_device_get_physdev_subsystem;
     kernel_device_class->get_physdev_manufacturer  = kernel_device_get_physdev_manufacturer;
     kernel_device_class->get_physdev_product       = kernel_device_get_physdev_product;
+    kernel_device_class->get_interface_number      = kernel_device_get_interface_number;
     kernel_device_class->get_interface_class       = kernel_device_get_interface_class;
     kernel_device_class->get_interface_subclass    = kernel_device_get_interface_subclass;
     kernel_device_class->get_interface_protocol    = kernel_device_get_interface_protocol;
@@ -764,6 +786,14 @@
                              G_PARAM_READWRITE);
     g_object_class_install_property (object_class, PROP_UDEV_DEVICE, properties[PROP_UDEV_DEVICE]);
 
+    properties[PROP_UDEV_CLIENT] =
+        g_param_spec_object ("udev-client",
+                             "udev client",
+                             "GUdev client",
+                             G_UDEV_TYPE_CLIENT,
+                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+    g_object_class_install_property (object_class, PROP_UDEV_CLIENT, properties[PROP_UDEV_CLIENT]);
+
     properties[PROP_PROPERTIES] =
         g_param_spec_object ("properties",
                              "Properties",
diff --git a/src/kerneldevice/mm-kernel-device-udev.h b/src/kerneldevice/mm-kernel-device-udev.h
index 34445b2..93153b3 100644
--- a/src/kerneldevice/mm-kernel-device-udev.h
+++ b/src/kerneldevice/mm-kernel-device-udev.h
@@ -48,8 +48,10 @@
 GType mm_kernel_device_udev_get_type (void);
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMKernelDeviceUdev, g_object_unref)
 
-MMKernelDevice *mm_kernel_device_udev_new                 (GUdevDevice              *udev_device);
-MMKernelDevice *mm_kernel_device_udev_new_from_properties (MMKernelEventProperties  *properties,
+MMKernelDevice *mm_kernel_device_udev_new                 (GUdevClient              *udev_client,
+                                                           GUdevDevice              *udev_device);
+MMKernelDevice *mm_kernel_device_udev_new_from_properties (GUdevClient              *udev_client,
+                                                           MMKernelEventProperties  *properties,
                                                            GError                  **error);
 
 #endif /* MM_KERNEL_DEVICE_UDEV_H */
diff --git a/src/kerneldevice/mm-kernel-device.c b/src/kerneldevice/mm-kernel-device.c
index 464a380..86251bc 100644
--- a/src/kerneldevice/mm-kernel-device.c
+++ b/src/kerneldevice/mm-kernel-device.c
@@ -126,6 +126,14 @@
 }
 
 gint
+mm_kernel_device_get_interface_number (MMKernelDevice *self)
+{
+    return (MM_KERNEL_DEVICE_GET_CLASS (self)->get_interface_number ?
+            MM_KERNEL_DEVICE_GET_CLASS (self)->get_interface_number (self) :
+            -1);
+}
+
+gint
 mm_kernel_device_get_interface_class (MMKernelDevice *self)
 {
     return (MM_KERNEL_DEVICE_GET_CLASS (self)->get_interface_class ?
diff --git a/src/kerneldevice/mm-kernel-device.h b/src/kerneldevice/mm-kernel-device.h
index 45b270d..f1d869b 100644
--- a/src/kerneldevice/mm-kernel-device.h
+++ b/src/kerneldevice/mm-kernel-device.h
@@ -41,6 +41,7 @@
     const gchar * (* get_driver)      (MMKernelDevice *self);
     const gchar * (* get_sysfs_path)  (MMKernelDevice *self);
 
+    gint          (* get_interface_number)      (MMKernelDevice *self);
     gint          (* get_interface_class)       (MMKernelDevice *self);
     gint          (* get_interface_subclass)    (MMKernelDevice *self);
     gint          (* get_interface_protocol)    (MMKernelDevice *self);
@@ -74,6 +75,7 @@
 const gchar *mm_kernel_device_get_driver      (MMKernelDevice *self);
 const gchar *mm_kernel_device_get_sysfs_path  (MMKernelDevice *self);
 
+gint         mm_kernel_device_get_interface_number      (MMKernelDevice *self);
 gint         mm_kernel_device_get_interface_class       (MMKernelDevice *self);
 gint         mm_kernel_device_get_interface_subclass    (MMKernelDevice *self);
 gint         mm_kernel_device_get_interface_protocol    (MMKernelDevice *self);
diff --git a/src/main.c b/src/main.c
index 928078a..d11383c 100644
--- a/src/main.c
+++ b/src/main.c
@@ -182,6 +182,9 @@
     mm_info ("ModemManager (version " MM_DIST_VERSION ") starting in %s bus...",
              mm_context_get_test_session () ? "session" : "system");
 
+    /* Detect runtime charset conversion support */
+    mm_modem_charsets_init ();
+
     /* Acquire name, don't allow replacement */
     name_id = g_bus_own_name (mm_context_get_test_session () ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM,
                               MM_DBUS_SERVICE,
diff --git a/src/mm-base-manager.c b/src/mm-base-manager.c
index 41236c4..f3f7e51 100644
--- a/src/mm-base-manager.c
+++ b/src/mm-base-manager.c
@@ -407,7 +407,7 @@
         g_autoptr(MMKernelDevice) kernel_device = NULL;
 #if defined WITH_UDEV
         if (!mm_context_get_test_no_udev ())
-            kernel_device = mm_kernel_device_udev_new_from_properties (properties, error);
+            kernel_device = mm_kernel_device_udev_new_from_properties (self->priv->udev, properties, error);
         else
 #endif
             kernel_device = mm_kernel_device_generic_new (properties, error);
@@ -436,7 +436,7 @@
     if (g_str_equal (action, "add") || g_str_equal (action, "move") || g_str_equal (action, "change")) {
         g_autoptr(MMKernelDevice) kernel_device = NULL;
 
-        kernel_device = mm_kernel_device_udev_new (device);
+        kernel_device = mm_kernel_device_udev_new (self->priv->udev, device);
         device_added (self, kernel_device, TRUE, FALSE);
         return;
     }
@@ -458,7 +458,7 @@
 {
     MMKernelDevice *kernel_device;
 
-    kernel_device = mm_kernel_device_udev_new (ctx->device);
+    kernel_device = mm_kernel_device_udev_new (ctx->self->priv->udev, ctx->device);
     device_added (ctx->self, kernel_device, FALSE, ctx->manual_scan);
     g_object_unref (kernel_device);
 
@@ -1493,13 +1493,12 @@
         return FALSE;
 
 #if defined WITH_UDEV
-    if (!mm_context_get_test_no_udev ()) {
-        /* Create udev client based on the subsystems requested by the plugins */
-        self->priv->udev = g_udev_client_new (mm_plugin_manager_get_subsystems (self->priv->plugin_manager));
-        /* If autoscan enabled, list for udev events */
-        if (self->priv->auto_scan)
-            g_signal_connect_swapped (self->priv->udev, "uevent", G_CALLBACK (handle_uevent), initable);
-    }
+    /* Create udev client based on the subsystems requested by the plugins */
+    self->priv->udev = g_udev_client_new (mm_plugin_manager_get_subsystems (self->priv->plugin_manager));
+
+    /* If autoscan enabled, list for udev events */
+    if (!mm_context_get_test_no_udev () && self->priv->auto_scan)
+        g_signal_connect_swapped (self->priv->udev, "uevent", G_CALLBACK (handle_uevent), initable);
 #endif
 
     /* Export the manager interface */
diff --git a/src/mm-base-modem.c b/src/mm-base-modem.c
index 78720f9..cfa50a2 100644
--- a/src/mm-base-modem.c
+++ b/src/mm-base-modem.c
@@ -834,21 +834,18 @@
 }
 
 GList *
-mm_base_modem_find_ports (MMBaseModem *self,
-                          MMPortSubsys subsys,
-                          MMPortType type,
-                          const gchar *name)
+mm_base_modem_find_ports (MMBaseModem  *self,
+                          MMPortSubsys  subsys,
+                          MMPortType    type)
 {
-    GList *out = NULL;
-    GHashTableIter iter;
-    gpointer value;
-    gpointer key;
+    GList          *out = NULL;
+    GHashTableIter  iter;
+    gpointer        value;
+    gpointer        key;
 
     if (!self->priv->ports)
         return NULL;
 
-    /* We'll iterate the ht of ports, looking for any port which is matches
-     * the compare function */
     g_hash_table_iter_init (&iter, self->priv->ports);
     while (g_hash_table_iter_next (&iter, &key, &value)) {
         MMPort *port = MM_PORT (value);
@@ -859,15 +856,44 @@
         if (type != MM_PORT_TYPE_UNKNOWN && mm_port_get_port_type (port) != type)
             continue;
 
-        if (name != NULL && !g_str_equal (mm_port_get_device (port), name))
-            continue;
-
         out = g_list_append (out, g_object_ref (port));
     }
 
     return g_list_sort (out, (GCompareFunc) port_cmp);
 }
 
+MMPort *
+mm_base_modem_peek_port (MMBaseModem *self,
+                         const gchar *name)
+{
+    GHashTableIter iter;
+    gpointer       value;
+    gpointer       key;
+
+    if (!self->priv->ports)
+        return NULL;
+
+    g_hash_table_iter_init (&iter, self->priv->ports);
+    while (g_hash_table_iter_next (&iter, &key, &value)) {
+        MMPort *port = MM_PORT (value);
+
+        if (g_str_equal (mm_port_get_device (port), name))
+            return port;
+    }
+
+    return NULL;
+}
+
+MMPort *
+mm_base_modem_get_port (MMBaseModem *self,
+                        const gchar *name)
+{
+    MMPort *port;
+
+    port = mm_base_modem_peek_port (self, name);
+    return (port ? g_object_ref (port) : NULL);
+}
+
 static void
 initialize_ready (MMBaseModem *self,
                   GAsyncResult *res)
diff --git a/src/mm-base-modem.h b/src/mm-base-modem.h
index 2463481..11c3993 100644
--- a/src/mm-base-modem.h
+++ b/src/mm-base-modem.h
@@ -142,10 +142,13 @@
 MMModemPortInfo *mm_base_modem_get_port_infos         (MMBaseModem *self,
                                                        guint *n_port_infos);
 
-GList            *mm_base_modem_find_ports            (MMBaseModem *self,
-                                                       MMPortSubsys subsys,
-                                                       MMPortType type,
-                                                       const gchar *name);
+GList            *mm_base_modem_find_ports            (MMBaseModem  *self,
+                                                       MMPortSubsys  subsys,
+                                                       MMPortType    type);
+MMPort           *mm_base_modem_peek_port             (MMBaseModem  *self,
+                                                       const gchar  *name);
+MMPort           *mm_base_modem_get_port              (MMBaseModem  *self,
+                                                       const gchar  *name);
 
 void     mm_base_modem_set_hotplugged (MMBaseModem *self,
                                        gboolean hotplugged);
diff --git a/src/mm-base-sim.c b/src/mm-base-sim.c
index 8505119..a3275fc 100644
--- a/src/mm-base-sim.c
+++ b/src/mm-base-sim.c
@@ -1141,6 +1141,101 @@
 }
 
 /*****************************************************************************/
+/* Preferred networks */
+
+static GList *
+parse_preferred_networks (const gchar  *response,
+                          GError      **error)
+{
+    gchar **entries;
+    gchar **iter;
+    GList  *result = NULL;
+
+    entries = g_strsplit_set (response, "\r\n", -1);
+    for (iter = entries; iter && *iter; iter++) {
+        gchar                   *operator_code = NULL;
+        gboolean                 gsm_act;
+        gboolean                 gsm_compact_act;
+        gboolean                 utran_act;
+        gboolean                 eutran_act;
+        gboolean                 ngran_act;
+        MMSimPreferredNetwork   *preferred_network = NULL;
+        MMModemAccessTechnology  act = MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;
+
+        g_strstrip (*iter);
+        if (strlen (*iter) == 0)
+            continue;
+
+        if (mm_sim_parse_cpol_query_response (*iter,
+                                              &operator_code,
+                                              &gsm_act,
+                                              &gsm_compact_act,
+                                              &utran_act,
+                                              &eutran_act,
+                                              &ngran_act,
+                                              error)) {
+            preferred_network = mm_sim_preferred_network_new ();
+            mm_sim_preferred_network_set_operator_code (preferred_network, operator_code);
+            if (gsm_act)
+                act |= MM_MODEM_ACCESS_TECHNOLOGY_GSM;
+            if (gsm_compact_act)
+                act |= MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT;
+            if (utran_act)
+                act |= MM_MODEM_ACCESS_TECHNOLOGY_UMTS;
+            if (eutran_act)
+                act |= MM_MODEM_ACCESS_TECHNOLOGY_LTE;
+            if (ngran_act)
+                act |= MM_MODEM_ACCESS_TECHNOLOGY_5GNR;
+            mm_sim_preferred_network_set_access_technology (preferred_network, act);
+            result = g_list_append (result, preferred_network);
+        } else
+            break;
+        g_free (operator_code);
+    }
+    g_strfreev (entries);
+
+    return result;
+}
+
+static GList *
+load_preferred_networks_finish (MMBaseSim     *self,
+                                GAsyncResult  *res,
+                                GError       **error)
+{
+    gchar *result;
+    GList *preferred_network_list;
+
+    result = g_task_propagate_pointer (G_TASK (res), error);
+    if (!result)
+        return NULL;
+
+    preferred_network_list = parse_preferred_networks (result, error);
+    mm_obj_dbg (self, "loaded %u preferred networks", g_list_length (preferred_network_list));
+
+    g_free (result);
+
+    return preferred_network_list;
+}
+
+STR_REPLY_READY_FN (load_preferred_networks)
+
+static void
+load_preferred_networks (MMBaseSim           *self,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+    mm_obj_dbg (self, "loading preferred networks...");
+
+    mm_base_modem_at_command (
+        self->priv->modem,
+        "+CPOL?",
+        20,
+        FALSE,
+        (GAsyncReadyCallback)load_preferred_networks_command_ready,
+        g_task_new (self, NULL, callback, user_data));
+}
+
+/*****************************************************************************/
 /* ICCID */
 
 static gchar *
@@ -1291,9 +1386,9 @@
 parse_mnc_length (const gchar *response,
                   GError **error)
 {
-    guint sw1 = 0;
-    guint sw2 = 0;
-    gchar *hex = 0;
+    guint             sw1 = 0;
+    guint             sw2 = 0;
+    g_autofree gchar *hex = NULL;
 
     if (!mm_3gpp_parse_crsm_response (response,
                                       &sw1,
@@ -1306,47 +1401,34 @@
         (sw1 == 0x91) ||
         (sw1 == 0x92) ||
         (sw1 == 0x9f)) {
-        gsize buflen = 0;
-        guint32 mnc_len;
-        gchar *bin;
+        gsize              buflen = 0;
+        guint32            mnc_len;
+        g_autofree guint8 *bin = NULL;
 
         /* Convert hex string to binary */
-        bin = mm_utils_hexstr2bin (hex, &buflen);
-        if (!bin || buflen < 4) {
-            g_set_error (error,
-                         MM_CORE_ERROR,
-                         MM_CORE_ERROR_FAILED,
-                         "SIM returned malformed response '%s'",
-                         hex);
-            g_free (bin);
-            g_free (hex);
+        bin = mm_utils_hexstr2bin (hex, -1, &buflen, error);
+        if (!bin) {
+            g_prefix_error (error, "SIM returned malformed response '%s': ", hex);
+            return 0;
+        }
+        if (buflen < 4) {
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "SIM returned malformed response '%s': too short", hex);
             return 0;
         }
 
-        g_free (hex);
-
         /* MNC length is byte 4 of this SIM file */
-        mnc_len = bin[3] & 0xFF;
-        if (mnc_len == 2 || mnc_len == 3) {
-            g_free (bin);
+        mnc_len = bin[3];
+        if (mnc_len == 2 || mnc_len == 3)
             return mnc_len;
-        }
 
-        g_set_error (error,
-                     MM_CORE_ERROR,
-                     MM_CORE_ERROR_FAILED,
-                     "SIM returned invalid MNC length %d (should be either 2 or 3)",
-                     mnc_len);
-        g_free (bin);
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "SIM returned invalid MNC length %d (should be either 2 or 3)", mnc_len);
         return 0;
     }
 
-    g_free (hex);
-    g_set_error (error,
-                 MM_CORE_ERROR,
-                 MM_CORE_ERROR_FAILED,
-                 "SIM failed to handle CRSM request (sw1 %d sw2 %d)",
-                 sw1, sw2);
+    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                 "SIM failed to handle CRSM request (sw1 %d sw2 %d)", sw1, sw2);
     return 0;
 }
 
@@ -1410,9 +1492,9 @@
 parse_spn (const gchar *response,
            GError **error)
 {
-    guint sw1 = 0;
-    guint sw2 = 0;
-    gchar *hex = 0;
+    guint             sw1 = 0;
+    guint             sw2 = 0;
+    g_autofree gchar *hex = NULL;
 
     if (!mm_3gpp_parse_crsm_response (response,
                                       &sw1,
@@ -1425,40 +1507,35 @@
         (sw1 == 0x91) ||
         (sw1 == 0x92) ||
         (sw1 == 0x9f)) {
-        gsize buflen = 0;
-        gchar *bin;
-        gchar *utf8;
+        g_autoptr(GByteArray)  bin_array = NULL;
+        g_autofree guint8     *bin = NULL;
+        gsize                  binlen = 0;
 
         /* Convert hex string to binary */
-        bin = mm_utils_hexstr2bin (hex, &buflen);
+        bin = mm_utils_hexstr2bin (hex, -1, &binlen, error);
         if (!bin) {
-            g_set_error (error,
-                         MM_CORE_ERROR,
-                         MM_CORE_ERROR_FAILED,
-                         "SIM returned malformed response '%s'",
-                         hex);
-            g_free (hex);
+            g_prefix_error (error, "SIM returned malformed response '%s': ", hex);
             return NULL;
         }
 
-        g_free (hex);
-
         /* Remove the FF filler at the end */
-        while (buflen > 1 && bin[buflen - 1] == (char)0xff)
-            buflen--;
+        while (binlen > 1 && bin[binlen - 1] == 0xff)
+            binlen--;
+        if (binlen <= 1) {
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                         "SIM returned empty response '%s'", hex);
+            return NULL;
+        }
+        /* Setup as bytearray.
+         * First byte is metadata; remainder is GSM-7 unpacked into octets; convert to UTF8 */
+        bin_array = g_byte_array_sized_new (binlen - 1);
+        g_byte_array_append (bin_array, bin + 1, binlen - 1);
 
-        /* First byte is metadata; remainder is GSM-7 unpacked into octets; convert to UTF8 */
-        utf8 = (gchar *)mm_charset_gsm_unpacked_to_utf8 ((guint8 *)bin + 1, buflen - 1);
-        g_free (bin);
-        return utf8;
+        return mm_modem_charset_bytearray_to_utf8 (bin_array, MM_MODEM_CHARSET_GSM, FALSE, error);
     }
 
-    g_free (hex);
-    g_set_error (error,
-                 MM_CORE_ERROR,
-                 MM_CORE_ERROR_FAILED,
-                 "SIM failed to handle CRSM request (sw1 %d sw2 %d)",
-                 sw1, sw2);
+    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                 "SIM failed to handle CRSM request (sw1 %d sw2 %d)", sw1, sw2);
     return NULL;
 }
 
@@ -1543,6 +1620,7 @@
     INITIALIZATION_STEP_OPERATOR_ID,
     INITIALIZATION_STEP_OPERATOR_NAME,
     INITIALIZATION_STEP_EMERGENCY_NUMBERS,
+    INITIALIZATION_STEP_PREFERRED_NETWORKS,
     INITIALIZATION_STEP_LAST
 } InitializationStep;
 
@@ -1641,6 +1719,31 @@
     interface_initialization_step (task);
 }
 
+static void
+init_load_preferred_networks_ready (MMBaseSim    *self,
+                                    GAsyncResult *res,
+                                    GTask        *task)
+{
+    InitAsyncContext *ctx;
+    GError           *error = NULL;
+    GList            *preferred_nets_list;
+
+    preferred_nets_list = MM_BASE_SIM_GET_CLASS (self)->load_preferred_networks_finish (self, res, &error);
+    if (error) {
+        mm_obj_warn (self, "couldn't load list of preferred networks: %s", error->message);
+        g_error_free (error);
+    }
+
+    mm_gdbus_sim_set_preferred_networks (MM_GDBUS_SIM (self),
+                                         mm_sim_preferred_network_list_get_variant (preferred_nets_list));
+    g_list_free_full (preferred_nets_list, (GDestroyNotify) mm_sim_preferred_network_free);
+
+    /* Go on to next step */
+    ctx = g_task_get_task_data (task);
+    ctx->step++;
+    interface_initialization_step (task);
+}
+
 #undef STR_REPLY_READY_FN
 #define STR_REPLY_READY_FN(NAME,DISPLAY)                                \
     static void                                                         \
@@ -1816,6 +1919,18 @@
         ctx->step++;
         /* Fall through */
 
+    case INITIALIZATION_STEP_PREFERRED_NETWORKS:
+        if (MM_BASE_SIM_GET_CLASS (self)->load_preferred_networks &&
+            MM_BASE_SIM_GET_CLASS (self)->load_preferred_networks_finish) {
+            MM_BASE_SIM_GET_CLASS (self)->load_preferred_networks (
+                self,
+                (GAsyncReadyCallback)init_load_preferred_networks_ready,
+                task);
+            return;
+        }
+        ctx->step++;
+        /* Fall through */
+
     case INITIALIZATION_STEP_LAST:
         /* We are done without errors! */
         g_task_return_boolean (task, TRUE);
@@ -2069,6 +2184,8 @@
     klass->load_operator_name_finish = load_operator_name_finish;
     klass->load_emergency_numbers = load_emergency_numbers;
     klass->load_emergency_numbers_finish = load_emergency_numbers_finish;
+    klass->load_preferred_networks = load_preferred_networks;
+    klass->load_preferred_networks_finish = load_preferred_networks_finish;
     klass->send_pin = send_pin;
     klass->send_pin_finish = common_send_pin_puk_finish;
     klass->send_puk = send_puk;
diff --git a/src/mm-base-sim.h b/src/mm-base-sim.h
index ed58252..67f2690 100644
--- a/src/mm-base-sim.h
+++ b/src/mm-base-sim.h
@@ -150,6 +150,14 @@
     /* Signals */
     void     (* pin_lock_enabled) (MMBaseSim *self,
                                    gboolean enabled);
+
+    /* Load preferred networks (async) */
+    void  (* load_preferred_networks)          (MMBaseSim *self,
+                                                GAsyncReadyCallback callback,
+                                                gpointer user_data);
+    GList * (* load_preferred_networks_finish) (MMBaseSim *self,
+                                                GAsyncResult *res,
+                                                GError **error);
 };
 
 GType mm_base_sim_get_type (void);
diff --git a/src/mm-bearer-mbim.c b/src/mm-bearer-mbim.c
index e53529e..b808f54 100644
--- a/src/mm-bearer-mbim.c
+++ b/src/mm-bearer-mbim.c
@@ -223,7 +223,8 @@
     MMBearerProperties *properties;
     ConnectStep step;
     MMPort *data;
-    MbimContextIpType ip_type;
+    MbimContextIpType requested_ip_type;
+    MbimContextIpType activated_ip_type;
     MMBearerConnectResult *connect_result;
 } ConnectContext;
 
@@ -396,9 +397,11 @@
         /* Build connection results */
 
         /* Build IPv4 config */
-        if (ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV4 ||
-            ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV4V6 ||
-            ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
+        if (ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV4 ||
+            ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV4V6 ||
+            ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
+            gboolean address_set = FALSE;
+
             ipv4_config = mm_bearer_ip_config_new ();
 
             /* We assume that if we have an IP we can use static configuration.
@@ -416,6 +419,7 @@
                 mm_bearer_ip_config_set_address (ipv4_config, str);
                 g_free (str);
                 g_object_unref (addr);
+                address_set = TRUE;
 
                 /* Netmask */
                 mm_bearer_ip_config_set_prefix (ipv4_config, ipv4address[0]->on_link_prefix_length);
@@ -451,13 +455,23 @@
             /* MTU */
             if (ipv4configurationavailable & MBIM_IP_CONFIGURATION_AVAILABLE_FLAG_MTU)
                 mm_bearer_ip_config_set_mtu (ipv4_config, ipv4mtu);
+
+            /* We requested IPv4, but it wasn't reported as activated. If there is no IP address
+             * provided by the modem, we assume the IPv4 bearer wasn't truly activated */
+            if (!address_set &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV4 &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV4V6 &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
+                mm_obj_dbg (self, "IPv4 requested but no IPv4 activated and no IPv4 address set: ignoring");
+                g_clear_object (&ipv4_config);
+            }
         } else
             ipv4_config = NULL;
 
         /* Build IPv6 config */
-        if (ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV6 ||
-            ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV4V6 ||
-            ctx->ip_type == MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
+        if (ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV6 ||
+            ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV4V6 ||
+            ctx->requested_ip_type == MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
             gboolean address_set = FALSE;
             gboolean gateway_set = FALSE;
             gboolean dns_set = FALSE;
@@ -528,6 +542,16 @@
                 mm_bearer_ip_config_set_method (ipv6_config, MM_BEARER_IP_METHOD_STATIC);
             else
                 mm_bearer_ip_config_set_method (ipv6_config, MM_BEARER_IP_METHOD_DHCP);
+
+            /* We requested IPv6, but it wasn't reported as activated. If there is no IPv6 address
+             * provided by the modem, we assume the IPv6 bearer wasn't truly activated */
+            if (!address_set &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV6 &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV4V6 &&
+                ctx->activated_ip_type != MBIM_CONTEXT_IP_TYPE_IPV4_AND_IPV6) {
+                mm_obj_dbg (self, "IPv6 requested but no IPv6 activated and no IPv6 address set: ignoring");
+                g_clear_object (&ipv6_config);
+            }
         } else
             ipv6_config = NULL;
 
@@ -570,7 +594,6 @@
     GError *error = NULL;
     MbimMessage *response;
     guint32 session_id;
-    MbimContextIpType ip_type;
     MbimActivationState activation_state;
     guint32 nw_error;
 
@@ -588,16 +611,16 @@
                 &session_id,
                 &activation_state,
                 NULL, /* voice_call_state */
-                &ip_type,
+                &ctx->activated_ip_type,
                 NULL, /* context_type */
                 &nw_error,
                 &inner_error)) {
             /* Report the IP type we asked for and the one returned by the modem */
-            mm_obj_dbg (self, "session ID '%u': %s (requested IP type: %s, received IP type: %s, nw error: %s)",
+            mm_obj_dbg (self, "session ID '%u': %s (requested IP type: %s, activated IP type: %s, nw error: %s)",
                         session_id,
                         mbim_activation_state_get_string (activation_state),
-                        mbim_context_ip_type_get_string (ctx->ip_type),
-                        mbim_context_ip_type_get_string (ip_type),
+                        mbim_context_ip_type_get_string (ctx->requested_ip_type),
+                        mbim_context_ip_type_get_string (ctx->activated_ip_type),
                         nw_error ? mbim_nw_error_get_string (nw_error) : "none");
             /* If the response reports an ACTIVATED state, we're good even if
              * there is a nw_error set (e.g. asking for IPv4v6 may return a
@@ -613,11 +636,6 @@
                                          "Unknown error: context activation failed");
                 }
             }
-            /* We're now connected, but we may have received an IP type different to the one
-             * requested (e.g. asking for IPv4v6 but received IPv4 only). Handle that, so that
-             * the next step getting IP details doesn't get confused. */
-            else if (ctx->ip_type != ip_type)
-                ctx->ip_type = ip_type;
         } else {
             /* Prefer the error from the result to the parsing error */
             if (!error)
@@ -995,14 +1013,15 @@
             g_free (str);
         }
 
-        ctx->ip_type = mm_bearer_ip_family_to_mbim_context_ip_type (ip_family, &error);
+        ctx->requested_ip_type = mm_bearer_ip_family_to_mbim_context_ip_type (ip_family, &error);
         if (error) {
             g_task_return_error (task, error);
             g_object_unref (task);
             return;
         }
 
-        mm_obj_dbg (self, "launching %s connection with APN '%s'...", mbim_context_ip_type_get_string (ctx->ip_type), apn);
+        mm_obj_dbg (self, "launching %s connection with APN '%s'...",
+                    mbim_context_ip_type_get_string (ctx->requested_ip_type), apn);
         message = (mbim_message_connect_set_new (
                        self->priv->session_id,
                        MBIM_ACTIVATION_COMMAND_ACTIVATE,
@@ -1011,7 +1030,7 @@
                        password ? password : "",
                        MBIM_COMPRESSION_NONE,
                        auth,
-                       ctx->ip_type,
+                       ctx->requested_ip_type,
                        mbim_uuid_from_context_type (MBIM_CONTEXT_TYPE_INTERNET),
                        &error));
         if (!message) {
@@ -1138,6 +1157,8 @@
     ctx->device = g_object_ref (device);;
     ctx->data = g_object_ref (data);
     ctx->step = CONNECT_STEP_FIRST;
+    ctx->requested_ip_type = MBIM_CONTEXT_IP_TYPE_DEFAULT;
+    ctx->activated_ip_type = MBIM_CONTEXT_IP_TYPE_DEFAULT;
 
     g_object_get (self,
                   MM_BASE_BEARER_CONFIG, &ctx->properties,
diff --git a/src/mm-broadband-modem-mbim.c b/src/mm-broadband-modem-mbim.c
index 02d524e..c524af4 100644
--- a/src/mm-broadband-modem-mbim.c
+++ b/src/mm-broadband-modem-mbim.c
@@ -238,8 +238,7 @@
 
     mbim_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                            MM_PORT_SUBSYS_UNKNOWN,
-                                           MM_PORT_TYPE_MBIM,
-                                           NULL);
+                                           MM_PORT_TYPE_MBIM);
 
     /* First MBIM port in the list is the primary one always */
     if (mbim_ports)
@@ -303,8 +302,7 @@
     /* Find the CDC-WDM port on the same USB interface as the given net port */
     cdc_wdm_mbim_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                                    MM_PORT_SUBSYS_USBMISC,
-                                                   MM_PORT_TYPE_MBIM,
-                                                   NULL);
+                                                   MM_PORT_TYPE_MBIM);
 
     for (l = cdc_wdm_mbim_ports; l && !found; l = g_list_next (l)) {
         const gchar *wdm_port_parent_path;
@@ -4889,30 +4887,23 @@
     g_autoptr(GByteArray) array = NULL;
 
     if (mm_charset_can_convert_to (command, MM_MODEM_CHARSET_GSM)) {
-        guint8  *gsm;
-        guint8  *packed;
-        guint32  len = 0;
-        guint32  packed_len = 0;
+        g_autoptr(GByteArray)  gsm = NULL;
+        guint8                *packed;
+        guint32                packed_len = 0;
 
         *scheme = MM_MODEM_GSM_USSD_SCHEME_7BIT;
-        gsm = mm_charset_utf8_to_unpacked_gsm (command, &len);
+        gsm = mm_modem_charset_bytearray_from_utf8 (command, MM_MODEM_CHARSET_GSM, FALSE, error);
         if (!gsm) {
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
-                         "Failed to encode USSD command in GSM7 charset");
+            g_prefix_error (error, "Failed to encode USSD command in GSM7 charset: ");
             return NULL;
         }
-        packed = mm_charset_gsm_pack (gsm, len, 0, &packed_len);
-        g_free (gsm);
-
+        packed = mm_charset_gsm_pack (gsm->data, gsm->len, 0, &packed_len);
         array = g_byte_array_new_take (packed, packed_len);
     } else {
-        g_autoptr(GError) inner_error = NULL;
-
         *scheme = MM_MODEM_GSM_USSD_SCHEME_UCS2;
-        array = g_byte_array_sized_new (strlen (command) * 2);
-        if (!mm_modem_charset_byte_array_append (array, command, FALSE, MM_MODEM_CHARSET_UCS2, &inner_error)) {
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
-                         "Failed to encode USSD command in UCS2 charset: %s", inner_error->message);
+        array = mm_modem_charset_bytearray_from_utf8 (command, MM_MODEM_CHARSET_UCS2, FALSE, error);
+        if (!array) {
+            g_prefix_error (error, "Failed to encode USSD command in UCS2 charset: ");
             return NULL;
         }
     }
@@ -4934,21 +4925,20 @@
     gchar *decoded = NULL;
 
     if (scheme == MM_MODEM_GSM_USSD_SCHEME_7BIT) {
-        g_autofree guint8  *unpacked = NULL;
-        guint32             unpacked_len;
+        g_autoptr(GByteArray)  unpacked_array = NULL;
+        guint8                *unpacked = NULL;
+        guint32                unpacked_len;
 
         unpacked = mm_charset_gsm_unpack ((const guint8 *)data->data, (data->len * 8) / 7, 0, &unpacked_len);
-        decoded = (gchar *) mm_charset_gsm_unpacked_to_utf8 (unpacked, unpacked_len);
+        unpacked_array = g_byte_array_new_take (unpacked, unpacked_len);
+
+        decoded = mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error);
         if (!decoded)
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
-                         "Error decoding USSD command in 0x%04x scheme (GSM7 charset)",
-                         scheme);
+            g_prefix_error (error, "Error decoding USSD command in 0x%04x scheme (GSM7 charset): ", scheme);
     } else if (scheme == MM_MODEM_GSM_USSD_SCHEME_UCS2) {
-        decoded = mm_modem_charset_byte_array_to_utf8 (data, MM_MODEM_CHARSET_UCS2);
+        decoded = mm_modem_charset_bytearray_to_utf8 (data, MM_MODEM_CHARSET_UCS2, FALSE, error);
         if (!decoded)
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
-                         "Error decoding USSD command in 0x%04x scheme (UCS2 charset)",
-                         scheme);
+            g_prefix_error (error, "Error decoding USSD command in 0x%04x scheme (UCS2 charset): ", scheme);
     } else
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                      "Failed to decode USSD command in unsupported 0x%04x scheme", scheme);
diff --git a/src/mm-broadband-modem-qmi.c b/src/mm-broadband-modem-qmi.c
index ba72f5f..f51146e 100644
--- a/src/mm-broadband-modem-qmi.c
+++ b/src/mm-broadband-modem-qmi.c
@@ -121,7 +121,7 @@
     GTask *activation_task;
 
     /* Messaging helpers */
-    gboolean messaging_fallback_at;
+    gboolean messaging_fallback_at_only;
     gboolean messaging_unsolicited_events_enabled;
     gboolean messaging_unsolicited_events_setup;
     guint messaging_event_report_indication_id;
@@ -206,8 +206,7 @@
 
     qmi_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                           MM_PORT_SUBSYS_UNKNOWN,
-                                          MM_PORT_TYPE_QMI,
-                                          NULL);
+                                          MM_PORT_TYPE_QMI);
 
     /* First QMI port in the list is the primary one always */
     if (qmi_ports)
@@ -273,8 +272,7 @@
     /* Find the CDC-WDM port on the same USB interface as the given net port */
     cdc_wdm_qmi_ports = mm_base_modem_find_ports (MM_BASE_MODEM (self),
                                                   MM_PORT_SUBSYS_USBMISC,
-                                                  MM_PORT_TYPE_QMI,
-                                                  NULL);
+                                                  MM_PORT_TYPE_QMI);
     for (l = cdc_wdm_qmi_ports; l && !found; l = g_list_next (l)) {
         const gchar *wdm_port_parent_path;
 
@@ -292,7 +290,7 @@
                      MM_CORE_ERROR_NOT_FOUND,
                      "Couldn't find associated QMI port for 'net/%s'",
                      mm_port_get_device (data));
-    else
+    else if (out_sio_port)
         *out_sio_port = QMI_SIO_PORT_NONE;
 
     return found;
@@ -5555,9 +5553,9 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    self->priv->messaging_fallback_at = iface_modem_messaging_parent->check_support_finish (_self, res, NULL);
+    self->priv->messaging_fallback_at_only = iface_modem_messaging_parent->check_support_finish (_self, res, NULL);
 
-    g_task_return_boolean (task, self->priv->messaging_fallback_at);
+    g_task_return_boolean (task, self->priv->messaging_fallback_at_only);
     g_object_unref (task);
 }
 
@@ -5602,8 +5600,8 @@
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
     MMSmsStorage supported;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->load_supported_storages_finish (_self, res, mem1, mem2, mem3, error);
     }
 
@@ -5630,8 +5628,8 @@
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
     GTask *task;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         iface_modem_messaging_parent->load_supported_storages (_self, callback, user_data);
         return;
     }
@@ -5651,8 +5649,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->setup_sms_format_finish (_self, res, error);
     }
 
@@ -5667,8 +5665,8 @@
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
     GTask *task;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->setup_sms_format (_self, callback, user_data);
     }
 
@@ -5688,8 +5686,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->set_default_storage_finish (_self, res, error);
     }
 
@@ -5732,8 +5730,8 @@
     GArray *routes_array;
     QmiMessageWmsSetRoutesInputRouteListElement route;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         iface_modem_messaging_parent->set_default_storage (_self, storage, callback, user_data);
         return;
     }
@@ -5817,8 +5815,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->load_initial_sms_parts_finish (_self, res, error);
     }
 
@@ -6196,8 +6194,8 @@
     GTask *task;
     QmiClient *client = NULL;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->load_initial_sms_parts (_self, storage, callback, user_data);
     }
 
@@ -6218,7 +6216,7 @@
 }
 
 /*****************************************************************************/
-/* Setup/Cleanup unsolicited event handlers (Messaging interface) */
+/* Common setup/cleanup unsolicited event handlers (Messaging interface) */
 
 typedef struct {
     MMIfaceModemMessaging *self;
@@ -6331,57 +6329,23 @@
 }
 
 static gboolean
-messaging_cleanup_unsolicited_events_finish (MMIfaceModemMessaging *_self,
-                                             GAsyncResult *res,
-                                             GError **error)
+common_setup_cleanup_messaging_unsolicited_events (MMBroadbandModemQmi  *self,
+                                                   gboolean              enable,
+                                                   GError              **error)
 {
-    MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
-
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
-        return iface_modem_messaging_parent->cleanup_unsolicited_events_finish (_self, res, error);
-    }
-
-    return g_task_propagate_boolean (G_TASK (res), error);
-}
-
-static gboolean
-messaging_setup_unsolicited_events_finish (MMIfaceModemMessaging *_self,
-                                             GAsyncResult *res,
-                                             GError **error)
-{
-    MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
-
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
-        return iface_modem_messaging_parent->setup_unsolicited_events_finish (_self, res, error);
-    }
-
-    return g_task_propagate_boolean (G_TASK (res), error);
-}
-
-static void
-common_setup_cleanup_messaging_unsolicited_events (MMBroadbandModemQmi *self,
-                                                   gboolean enable,
-                                                   GAsyncReadyCallback callback,
-                                                   gpointer user_data)
-{
-    GTask *task;
     QmiClient *client = NULL;
 
-    if (!mm_shared_qmi_ensure_client (MM_SHARED_QMI (self),
-                                      QMI_SERVICE_WMS, &client,
-                                      callback, user_data))
-        return;
-
-    task = g_task_new (self, NULL, callback, user_data);
+    client = mm_shared_qmi_peek_client (MM_SHARED_QMI (self),
+                                        QMI_SERVICE_WMS,
+                                        MM_PORT_QMI_FLAG_DEFAULT,
+                                        error);
+    if (!client)
+        return FALSE;
 
     if (enable == self->priv->messaging_unsolicited_events_setup) {
         mm_obj_dbg (self, "messaging unsolicited events already %s; skipping",
                     enable ? "setup" : "cleanup");
-        g_task_return_boolean (task, TRUE);
-        g_object_unref (task);
-        return;
+        return TRUE;
     }
 
     /* Store new state */
@@ -6401,44 +6365,119 @@
         self->priv->messaging_event_report_indication_id = 0;
     }
 
-    g_task_return_boolean (task, TRUE);
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Cleanup unsolicited event handlers (Messaging interface) */
+
+static gboolean
+messaging_cleanup_unsolicited_events_finish (MMIfaceModemMessaging  *self,
+                                             GAsyncResult           *res,
+                                             GError                **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+parent_messaging_cleanup_unsolicited_events_ready (MMIfaceModemMessaging *_self,
+                                                   GAsyncResult          *res,
+                                                   GTask                 *task)
+{
+    MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
+    GError              *error = NULL;
+
+    if (!iface_modem_messaging_parent->cleanup_unsolicited_events_finish (_self, res, &error)) {
+        if (self->priv->messaging_fallback_at_only) {
+            g_task_return_error (task, error);
+            g_object_unref (task);
+            return;
+        }
+        mm_obj_dbg (self, "cleaning up parent messaging unsolicited events failed: %s", error->message);
+        g_clear_error (&error);
+    }
+
+    /* handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Disable QMI indications */
+    if (!common_setup_cleanup_messaging_unsolicited_events (self, FALSE, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
     g_object_unref (task);
 }
 
 static void
-messaging_cleanup_unsolicited_events (MMIfaceModemMessaging *_self,
-                                      GAsyncReadyCallback callback,
-                                      gpointer user_data)
+messaging_cleanup_unsolicited_events (MMIfaceModemMessaging *self,
+                                      GAsyncReadyCallback    callback,
+                                      gpointer               user_data)
 {
-    MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
+    /* Disable AT URCs parent and chain QMI indications disabling */
+    iface_modem_messaging_parent->cleanup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_messaging_cleanup_unsolicited_events_ready,
+        g_task_new (self, NULL, callback, user_data));
+}
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
-        return iface_modem_messaging_parent->cleanup_unsolicited_events (_self, callback, user_data);
-    }
+/*****************************************************************************/
+/* Setup unsolicited event handlers (Messaging interface) */
 
-    common_setup_cleanup_messaging_unsolicited_events (MM_BROADBAND_MODEM_QMI (self),
-                                                       FALSE,
-                                                       callback,
-                                                       user_data);
+static gboolean
+messaging_setup_unsolicited_events_finish (MMIfaceModemMessaging  *self,
+                                           GAsyncResult           *res,
+                                           GError                **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
 }
 
 static void
-messaging_setup_unsolicited_events (MMIfaceModemMessaging *_self,
-                                    GAsyncReadyCallback callback,
-                                    gpointer user_data)
+parent_messaging_setup_unsolicited_events_ready (MMIfaceModemMessaging *_self,
+                                                 GAsyncResult          *res,
+                                                 GTask                 *task)
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
+    GError              *error = NULL;
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
-        return iface_modem_messaging_parent->setup_unsolicited_events (_self, callback, user_data);
+    if (!iface_modem_messaging_parent->setup_unsolicited_events_finish (_self, res, &error)) {
+        if (self->priv->messaging_fallback_at_only) {
+            g_task_return_error (task, error);
+            g_object_unref (task);
+            return;
+        }
+        mm_obj_dbg (self, "setting up parent messaging unsolicited events failed: %s", error->message);
+        g_clear_error (&error);
     }
 
-    common_setup_cleanup_messaging_unsolicited_events (MM_BROADBAND_MODEM_QMI (self),
-                                                       TRUE,
-                                                       callback,
-                                                       user_data);
+    /* handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Enable QMI indications */
+    if (!common_setup_cleanup_messaging_unsolicited_events (self, TRUE, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static void
+messaging_setup_unsolicited_events (MMIfaceModemMessaging *self,
+                                    GAsyncReadyCallback    callback,
+                                    gpointer               user_data)
+{
+    /* Enable AT URCs parent and chain QMI indication enabling */
+    iface_modem_messaging_parent->setup_unsolicited_events (
+        self,
+        (GAsyncReadyCallback)parent_messaging_setup_unsolicited_events_ready,
+        g_task_new (self, NULL, callback, user_data));
 }
 
 /*****************************************************************************/
@@ -6455,8 +6494,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at && iface_modem_messaging_parent->disable_unsolicited_events_finish) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only && iface_modem_messaging_parent->disable_unsolicited_events_finish) {
         return iface_modem_messaging_parent->disable_unsolicited_events_finish (_self, res, error);
     }
 
@@ -6470,8 +6509,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->enable_unsolicited_events_finish (_self, res, error);
     }
 
@@ -6563,8 +6602,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         /* Generic implementation doesn't actually have a method to disable
          * unsolicited messaging events */
         if (!iface_modem_messaging_parent->disable_unsolicited_events) {
@@ -6592,8 +6631,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->enable_unsolicited_events (_self, callback, user_data);
     }
 
@@ -6611,8 +6650,8 @@
 {
     MMBroadbandModemQmi *self = MM_BROADBAND_MODEM_QMI (_self);
 
-    /* Handle fallback */
-    if (self->priv->messaging_fallback_at) {
+    /* Handle AT URC only fallback */
+    if (self->priv->messaging_fallback_at_only) {
         return iface_modem_messaging_parent->create_sms (_self);
     }
 
@@ -7477,8 +7516,8 @@
         return (GArray *) g_steal_pointer (&barray);
     }
 
-    barray = g_byte_array_sized_new (command_len * 2);
-    if (!mm_modem_charset_byte_array_append (barray, command, FALSE, MM_MODEM_CHARSET_UCS2, &inner_error)) {
+    barray = mm_modem_charset_bytearray_from_utf8 (command, MM_MODEM_CHARSET_UCS2, FALSE, &inner_error);
+    if (!barray) {
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                      "Failed to encode USSD command in UCS2 charset: %s", inner_error->message);
         return NULL;
@@ -7502,11 +7541,9 @@
                          "Error decoding USSD command in 0x%04x scheme (ASCII charset)",
                          scheme);
     } else if (scheme == QMI_VOICE_USS_DATA_CODING_SCHEME_UCS2) {
-        decoded = mm_modem_charset_byte_array_to_utf8 ((GByteArray *) data, MM_MODEM_CHARSET_UCS2);
+        decoded = mm_modem_charset_bytearray_to_utf8 ((GByteArray *) data, MM_MODEM_CHARSET_UCS2, FALSE, error);
         if (!decoded)
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
-                         "Error decoding USSD command in 0x%04x scheme (UCS2 charset)",
-                         scheme);
+            g_prefix_error (error, "Error decoding USSD command in 0x%04x scheme (UCS2 charset): ", scheme);
     } else
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                      "Failed to decode USSD command in unsupported 0x%04x scheme", scheme);
diff --git a/src/mm-broadband-modem.c b/src/mm-broadband-modem.c
index 97c60b6..a25883a 100644
--- a/src/mm-broadband-modem.c
+++ b/src/mm-broadband-modem.c
@@ -4465,7 +4465,7 @@
                                            error))
         return NULL;
 
-    mm_3gpp_normalize_operator (&operator_code, MM_BROADBAND_MODEM (self)->priv->modem_current_charset);
+    mm_3gpp_normalize_operator (&operator_code, MM_BROADBAND_MODEM (self)->priv->modem_current_charset, self);
     if (operator_code)
         mm_obj_dbg (self, "loaded Operator Code: %s", operator_code);
     return operator_code;
@@ -4504,7 +4504,7 @@
                                            error))
         return NULL;
 
-    mm_3gpp_normalize_operator (&operator_name, MM_BROADBAND_MODEM (self)->priv->modem_current_charset);
+    mm_3gpp_normalize_operator (&operator_name, MM_BROADBAND_MODEM (self)->priv->modem_current_charset, self);
     if (operator_name)
         mm_obj_dbg (self, "loaded Operator Name: %s", operator_name);
     return operator_name;
@@ -4800,27 +4800,37 @@
 }
 
 static void
-cops_ascii_set_ready (MMBaseModem  *self,
+cops_ascii_set_ready (MMBaseModem  *_self,
                       GAsyncResult *res,
                       GTask        *task)
 {
-    GError *error = NULL;
+    MMBroadbandModem  *self = MM_BROADBAND_MODEM (_self);
+    g_autoptr(GError)  error = NULL;
 
-    if (!mm_base_modem_at_command_full_finish (MM_BASE_MODEM (self), res, &error)) {
+    if (!mm_base_modem_at_command_full_finish (_self, res, &error)) {
         /* If it failed with an unsupported error, retry with current modem charset */
         if (g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_NOT_SUPPORTED)) {
-            gchar *operator_id;
-            gchar *operator_id_current_charset;
+            g_autoptr(GError)  enc_error = NULL;
+            g_autofree gchar  *operator_id_enc = NULL;
+            gchar             *operator_id;
 
+            /* try to encode to current charset */
             operator_id = g_task_get_task_data (task);
-            operator_id_current_charset = mm_broadband_modem_take_and_convert_to_current_charset (MM_BROADBAND_MODEM (self), g_strdup (operator_id));
+            operator_id_enc = mm_modem_charset_str_from_utf8 (operator_id, self->priv->modem_current_charset, FALSE, &enc_error);
+            if (!operator_id_enc) {
+                mm_obj_dbg (self, "couldn't convert operator id to current charset: %s", enc_error->message);
+                g_task_return_error (task, g_steal_pointer (&error));
+                g_object_unref (task);
+                return;
+            }
 
-            if (g_strcmp0 (operator_id, operator_id_current_charset) != 0) {
-                gchar *command;
+            /* retry only if encoded string is different to the non-encoded one */
+            if (g_strcmp0 (operator_id, operator_id_enc) != 0) {
+                g_autofree gchar *command = NULL;
 
-                command = g_strdup_printf ("+COPS=1,2,\"%s\"", operator_id_current_charset);
-                mm_base_modem_at_command_full (MM_BASE_MODEM (self),
-                                               mm_base_modem_peek_best_at_port (MM_BASE_MODEM (self), NULL),
+                command = g_strdup_printf ("+COPS=1,2,\"%s\"", operator_id_enc);
+                mm_base_modem_at_command_full (_self,
+                                               mm_base_modem_peek_best_at_port (_self, NULL),
                                                command,
                                                120,
                                                FALSE,
@@ -4828,16 +4838,10 @@
                                                g_task_get_cancellable (task),
                                                (GAsyncReadyCallback)cops_set_ready,
                                                task);
-                g_error_free (error);
-                g_free (operator_id_current_charset);
-                g_free (command);
                 return;
             }
-            /* operator id string would be the same on the current charset,
-             * so fallback and return the not supported error */
-            g_free (operator_id_current_charset);
         }
-        g_task_return_error (task, error);
+        g_task_return_error (task, g_steal_pointer (&error));
     } else
         g_task_return_boolean (task, TRUE);
     g_object_unref (task);
@@ -5877,47 +5881,53 @@
 /* USSD Encode/Decode (3GPP/USSD interface) */
 
 static gchar *
-modem_3gpp_ussd_encode (MMIfaceModem3gppUssd  *self,
+modem_3gpp_ussd_encode (MMIfaceModem3gppUssd  *_self,
                         const gchar           *command,
                         guint                 *scheme,
                         GError               **error)
 {
-    MMBroadbandModem      *broadband = MM_BROADBAND_MODEM (self);
-    gchar                 *hex = NULL;
+    MMBroadbandModem      *self = MM_BROADBAND_MODEM (_self);
     g_autoptr(GByteArray)  ussd_command = NULL;
 
-    ussd_command = g_byte_array_new ();
-
     /* Encode to the current charset (as per AT+CSCS, which is what most modems
      * (except for Huawei it seems) will ask for. */
-    if (mm_modem_charset_byte_array_append (ussd_command,
-                                            command,
-                                            FALSE,
-                                            broadband->priv->modem_current_charset,
-                                            NULL)) {
-        /* The scheme value does NOT represent the encoding used to encode the string
-         * we're giving. This scheme reflects the encoding that the modem should use when
-         * sending the data out to the network. We're hardcoding this to GSM-7 because
-         * USSD commands fit well in GSM-7, unlike USSD responses that may contain code
-         * points that may only be encoded in UCS-2. */
-        *scheme = MM_MODEM_GSM_USSD_SCHEME_7BIT;
-        /* convert to hex representation */
-        hex = mm_utils_bin2hexstr (ussd_command->data, ussd_command->len);
+    ussd_command = mm_modem_charset_bytearray_from_utf8 (command, self->priv->modem_current_charset, FALSE, error);
+    if (!ussd_command) {
+        g_prefix_error (error, "Failed to encode USSD command: ");
+        return NULL;
     }
 
-    return hex;
+    /* The scheme value does NOT represent the encoding used to encode the string
+     * we're giving. This scheme reflects the encoding that the modem should use when
+     * sending the data out to the network. We're hardcoding this to GSM-7 because
+     * USSD commands fit well in GSM-7, unlike USSD responses that may contain code
+     * points that may only be encoded in UCS-2. */
+    *scheme = MM_MODEM_GSM_USSD_SCHEME_7BIT;
+
+    /* convert to hex representation */
+    return (gchar *) mm_utils_bin2hexstr (ussd_command->data, ussd_command->len);
 }
 
 static gchar *
-modem_3gpp_ussd_decode (MMIfaceModem3gppUssd *self,
-                        const gchar *reply,
-                        GError **error)
+modem_3gpp_ussd_decode (MMIfaceModem3gppUssd  *self,
+                        const gchar           *reply,
+                        GError               **error)
 {
-    MMBroadbandModem *broadband = MM_BROADBAND_MODEM (self);
+    MMBroadbandModem      *broadband = MM_BROADBAND_MODEM (self);
+    guint8                *bin = NULL;
+    gsize                  bin_len = 0;
+    g_autoptr(GByteArray)  barray = NULL;
+
+    bin = (guint8 *) mm_utils_hexstr2bin (reply, -1, &bin_len, error);
+    if (!bin) {
+        g_prefix_error (error, "Couldn't convert HEX string to binary: ");
+        return NULL;
+    }
+    barray = g_byte_array_new_take (bin, bin_len);
 
     /* Decode from current charset (as per AT+CSCS, which is what most modems
      * (except for Huawei it seems) will ask for. */
-    return mm_modem_charset_hex_to_utf8 (reply, broadband->priv->modem_current_charset);
+    return mm_modem_charset_bytearray_to_utf8 (barray, broadband->priv->modem_current_charset, FALSE, error);
 }
 
 /*****************************************************************************/
@@ -7305,11 +7315,17 @@
     ctx = g_task_get_task_data (task);
 
     while (g_match_info_matches (match_info)) {
-        MMSmsPart *part;
-        guint matches, idx;
-        gchar *number, *timestamp, *text, *ucs2_text, *stat;
-        gsize ucs2_len = 0;
-        GByteArray *raw;
+        MMSmsPart            *part;
+        guint                 matches;
+        guint                 idx;
+        g_autofree gchar      *number_enc = NULL;
+        g_autofree gchar      *number = NULL;
+        g_autofree gchar      *timestamp = NULL;
+        g_autofree gchar      *text_enc = NULL;
+        g_autofree gchar      *text = NULL;
+        g_autofree gchar      *stat = NULL;
+        g_autoptr(GByteArray)  raw = NULL;
+        g_autoptr(GError)      inner_error = NULL;
 
         matches = g_match_info_get_match_count (match_info);
         if (matches != 7) {
@@ -7330,39 +7346,44 @@
         }
 
         /* Get and parse number */
-        number = mm_get_string_unquoted_from_match_info (match_info, 3);
-        if (!number) {
+        number_enc = mm_get_string_unquoted_from_match_info (match_info, 3);
+        if (!number_enc) {
             mm_obj_dbg (self, "failed to get message sender number");
-            g_free (stat);
+            goto next;
+        }
+        number = mm_modem_charset_str_to_utf8 (number_enc, -1, self->priv->modem_current_charset, FALSE, &inner_error);
+        if (!number) {
+            mm_obj_dbg (self, "failed to convert message sender number to UTF-8: %s", inner_error->message);
             goto next;
         }
 
-        number = mm_broadband_modem_take_and_convert_to_utf8 (MM_BROADBAND_MODEM (self),
-                                                              number);
-
         /* Get and parse timestamp (always expected in ASCII) */
         timestamp = mm_get_string_unquoted_from_match_info (match_info, 5);
+        if (timestamp && !g_str_is_ascii (timestamp)) {
+            mm_obj_dbg (self, "failed to parse input timestamp as ASCII");
+            goto next;
+        }
 
         /* Get and parse text */
-        text = mm_broadband_modem_take_and_convert_to_utf8 (MM_BROADBAND_MODEM (self),
-                                                            g_match_info_fetch (match_info, 6));
+        text_enc = g_match_info_fetch (match_info, 6);
+        text = mm_modem_charset_str_to_utf8 (text_enc, -1, self->priv->modem_current_charset, FALSE, &inner_error);
+        if (!text) {
+            mm_obj_dbg (self, "failed to convert message text to UTF-8: %s", inner_error->message);
+            goto next;
+        }
 
         /* The raw SMS data can only be GSM, UCS2, or unknown (8-bit), so we
          * need to convert to UCS2 here.
          */
-        ucs2_text = g_convert (text, -1, "UCS-2BE//TRANSLIT", "UTF-8", NULL, &ucs2_len, NULL);
-        g_assert (ucs2_text);
-        raw = g_byte_array_sized_new (ucs2_len);
-        g_byte_array_append (raw, (const guint8 *) ucs2_text, ucs2_len);
-        g_free (ucs2_text);
+        raw = mm_modem_charset_bytearray_from_utf8 (text, MM_MODEM_CHARSET_UCS2, FALSE, NULL);
+        g_assert (raw);
 
         /* all take() methods pass ownership of the value as well */
-        part = mm_sms_part_new (idx,
-                                sms_pdu_type_from_str (stat));
-        mm_sms_part_take_number (part, number);
-        mm_sms_part_take_timestamp (part, timestamp);
-        mm_sms_part_take_text (part, text);
-        mm_sms_part_take_data (part, raw);
+        part = mm_sms_part_new (idx, sms_pdu_type_from_str (stat));
+        mm_sms_part_take_number (part, g_steal_pointer (&number));
+        mm_sms_part_take_timestamp (part, g_steal_pointer (&timestamp));
+        mm_sms_part_take_text (part, g_steal_pointer (&text));
+        mm_sms_part_take_data (part, g_steal_pointer (&raw));
         mm_sms_part_set_class (part, -1);
 
         mm_obj_dbg (self, "correctly parsed SMS list entry (%d)", idx);
@@ -7370,7 +7391,6 @@
                                             part,
                                             sms_state_from_str (stat),
                                             ctx->list_storage);
-        g_free (stat);
 next:
         g_match_info_next (match_info, NULL);
     }
@@ -11884,35 +11904,14 @@
 
 /*****************************************************************************/
 
-gchar *
-mm_broadband_modem_take_and_convert_to_utf8 (MMBroadbandModem *self,
-                                             gchar *str)
-{
-    /* should only be used AFTER current charset is set */
-    if (self->priv->modem_current_charset == MM_MODEM_CHARSET_UNKNOWN)
-        return str;
-
-    return mm_charset_take_and_convert_to_utf8 (str,
-                                                self->priv->modem_current_charset);
-}
-
-gchar *
-mm_broadband_modem_take_and_convert_to_current_charset (MMBroadbandModem *self,
-                                                        gchar *str)
-{
-    /* should only be used AFTER current charset is set */
-    if (self->priv->modem_current_charset == MM_MODEM_CHARSET_UNKNOWN)
-        return str;
-
-    return mm_utf8_take_and_convert_to_charset (str, self->priv->modem_current_charset);
-}
-
 MMModemCharset
 mm_broadband_modem_get_current_charset (MMBroadbandModem *self)
 {
     return self->priv->modem_current_charset;
 }
 
+/*****************************************************************************/
+
 gchar *
 mm_broadband_modem_create_device_identifier (MMBroadbandModem *self,
                                              const gchar *ati,
@@ -11934,7 +11933,6 @@
                     MM_GDBUS_MODEM (self->priv->modem_dbus_skeleton))));
 }
 
-
 /*****************************************************************************/
 
 void
diff --git a/src/mm-broadband-modem.h b/src/mm-broadband-modem.h
index eafca85..1f5acac 100644
--- a/src/mm-broadband-modem.h
+++ b/src/mm-broadband-modem.h
@@ -100,18 +100,6 @@
                                           guint16 vendor_id,
                                           guint16 product_id);
 
-/* Convert the given string, which comes in the charset currently set in the
- * modem, to UTF-8. Given in the API so that subclasses can also use it directly.
- */
-gchar *mm_broadband_modem_take_and_convert_to_utf8 (MMBroadbandModem *self,
-                                                    gchar *str);
-
-/* Convert the given string, which comes in UTF-8, to the charset currently set
- * in the modem. Given in the API so that subclasses can also use it directly.
- */
-gchar *mm_broadband_modem_take_and_convert_to_current_charset (MMBroadbandModem *self,
-                                                               gchar *str);
-
 MMModemCharset mm_broadband_modem_get_current_charset (MMBroadbandModem *self);
 
 /* Create a unique device identifier string using the ATI and ATI1 replies and some
diff --git a/src/mm-charsets.c b/src/mm-charsets.c
index 19d1874..a48da36 100644
--- a/src/mm-charsets.c
+++ b/src/mm-charsets.c
@@ -11,6 +11,7 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2010 Red Hat, Inc.
+ * Copyright (C) 2020 Aleksander Morgado <aleksander@aleksander.es>
  */
 
 #include <config.h>
@@ -26,24 +27,28 @@
 #include "mm-charsets.h"
 #include "mm-log.h"
 
+/* Common fallback character when transliteration is enabled */
+static const gchar *translit_fallback = "?";
+
+/******************************************************************************/
+/* Expected charset settings */
+
 typedef struct {
+    MMModemCharset  charset;
     const gchar    *gsm_name;
     const gchar    *other_name;
-    const gchar    *iconv_from_name;
-    const gchar    *iconv_to_name;
-    MMModemCharset  charset;
-} CharsetEntry;
+    const gchar    *iconv_name;
+} CharsetSettings;
 
-static const CharsetEntry charset_map[] = {
-    { "UTF-8",   "UTF8",   "UTF-8",     "UTF-8//TRANSLIT",     MM_MODEM_CHARSET_UTF8    },
-    { "UCS2",    NULL,     "UCS-2BE",   "UCS-2BE//TRANSLIT",   MM_MODEM_CHARSET_UCS2    },
-    { "IRA",     "ASCII",  "ASCII",     "ASCII//TRANSLIT",     MM_MODEM_CHARSET_IRA     },
-    { "GSM",     NULL,     NULL,        NULL,                  MM_MODEM_CHARSET_GSM     },
-    { "8859-1",  NULL,     "ISO8859-1", "ISO8859-1//TRANSLIT", MM_MODEM_CHARSET_8859_1  },
-    { "PCCP437", "CP437",  "CP437",     "CP437//TRANSLIT",     MM_MODEM_CHARSET_PCCP437 },
-    { "PCDN",    "CP850",  "CP850",     "CP850//TRANSLIT",     MM_MODEM_CHARSET_PCDN    },
-    { "HEX",     NULL,     NULL,        NULL,                  MM_MODEM_CHARSET_HEX     },
-    { "UTF-16",  "UTF16",  "UTF-16BE",  "UTF-16BE//TRANSLIT",  MM_MODEM_CHARSET_UTF16   },
+static const CharsetSettings charset_settings[] = {
+    { MM_MODEM_CHARSET_UTF8,    "UTF-8",   "UTF8",   "UTF-8"     },
+    { MM_MODEM_CHARSET_UCS2,    "UCS2",    NULL,     "UCS-2BE"   },
+    { MM_MODEM_CHARSET_IRA,     "IRA",     "ASCII",  "ASCII"     },
+    { MM_MODEM_CHARSET_GSM,     "GSM",     NULL,     NULL        },
+    { MM_MODEM_CHARSET_8859_1,  "8859-1",  NULL,     "ISO8859-1" },
+    { MM_MODEM_CHARSET_PCCP437, "PCCP437", "CP437",  "CP437"     },
+    { MM_MODEM_CHARSET_PCDN,    "PCDN",    "CP850",  "CP850"     },
+    { MM_MODEM_CHARSET_UTF16,   "UTF-16",  "UTF16",  "UTF-16BE"  },
 };
 
 MMModemCharset
@@ -53,24 +58,24 @@
 
     g_return_val_if_fail (string != NULL, MM_MODEM_CHARSET_UNKNOWN);
 
-    for (i = 0; i < G_N_ELEMENTS (charset_map); i++) {
-        if (strcasestr (string, charset_map[i].gsm_name))
-            return charset_map[i].charset;
-        if (charset_map[i].other_name && strcasestr (string, charset_map[i].other_name))
-            return charset_map[i].charset;
+    for (i = 0; i < G_N_ELEMENTS (charset_settings); i++) {
+        if (strcasestr (string, charset_settings[i].gsm_name))
+            return charset_settings[i].charset;
+        if (charset_settings[i].other_name && strcasestr (string, charset_settings[i].other_name))
+            return charset_settings[i].charset;
     }
     return MM_MODEM_CHARSET_UNKNOWN;
 }
 
-static const CharsetEntry *
-lookup_charset_by_id (MMModemCharset charset)
+static const CharsetSettings *
+lookup_charset_settings (MMModemCharset charset)
 {
     guint i;
 
     g_return_val_if_fail (charset != MM_MODEM_CHARSET_UNKNOWN, NULL);
-    for (i = 0; i < G_N_ELEMENTS (charset_map); i++) {
-        if (charset_map[i].charset == charset)
-            return &charset_map[i];
+    for (i = 0; i < G_N_ELEMENTS (charset_settings); i++) {
+        if (charset_settings[i].charset == charset)
+            return &charset_settings[i];
     }
     g_warn_if_reached ();
     return NULL;
@@ -79,146 +84,13 @@
 const gchar *
 mm_modem_charset_to_string (MMModemCharset charset)
 {
-    const CharsetEntry *entry;
+    const CharsetSettings *settings;
 
-    entry = lookup_charset_by_id (charset);
-    return entry ? entry->gsm_name : NULL;
+    settings = lookup_charset_settings (charset);
+    return settings ? settings->gsm_name : NULL;
 }
 
-static const gchar *
-charset_iconv_to (MMModemCharset charset)
-{
-    const CharsetEntry *entry;
-
-    entry = lookup_charset_by_id (charset);
-    return entry ? entry->iconv_to_name : NULL;
-}
-
-static const gchar *
-charset_iconv_from (MMModemCharset charset)
-{
-    const CharsetEntry *entry;
-
-    entry = lookup_charset_by_id (charset);
-    return entry ? entry->iconv_from_name : NULL;
-}
-
-gboolean
-mm_modem_charset_byte_array_append (GByteArray      *array,
-                                    const gchar     *utf8,
-                                    gboolean         quoted,
-                                    MMModemCharset   charset,
-                                    GError         **error)
-{
-    g_autofree gchar *converted = NULL;
-    const gchar      *iconv_to;
-    gsize             written = 0;
-
-    g_return_val_if_fail (array != NULL, FALSE);
-    g_return_val_if_fail (utf8 != NULL, FALSE);
-
-    iconv_to = charset_iconv_to (charset);
-    g_assert (iconv_to);
-
-    converted = g_convert (utf8, -1, iconv_to, "UTF-8", NULL, &written, error);
-    if (!converted) {
-        g_prefix_error (error, "Failed to convert '%s' to %s character set",
-                        utf8, iconv_to);
-        return FALSE;
-    }
-
-    if (quoted)
-        g_byte_array_append (array, (const guint8 *) "\"", 1);
-    g_byte_array_append (array, (const guint8 *) converted, written);
-    if (quoted)
-        g_byte_array_append (array, (const guint8 *) "\"", 1);
-
-    return TRUE;
-}
-
-gchar *
-mm_modem_charset_byte_array_to_utf8 (GByteArray     *array,
-                                     MMModemCharset  charset)
-{
-    const gchar       *iconv_from;
-    g_autofree gchar  *converted = NULL;
-    g_autoptr(GError)  error = NULL;
-
-    g_return_val_if_fail (array != NULL, NULL);
-    g_return_val_if_fail (charset != MM_MODEM_CHARSET_UNKNOWN, NULL);
-
-    iconv_from = charset_iconv_from (charset);
-    g_return_val_if_fail (iconv_from != NULL, FALSE);
-
-    converted = g_convert ((const gchar *)array->data, array->len,
-                           "UTF-8//TRANSLIT", iconv_from,
-                           NULL, NULL, &error);
-    if (!converted || error)
-        return NULL;
-
-    return g_steal_pointer (&converted);
-}
-
-gchar *
-mm_modem_charset_hex_to_utf8 (const gchar    *src,
-                              MMModemCharset  charset)
-{
-    const gchar      *iconv_from;
-    g_autofree gchar *unconverted = NULL;
-    g_autofree gchar *converted = NULL;
-    g_autoptr(GError) error = NULL;
-    gsize             unconverted_len = 0;
-
-    g_return_val_if_fail (src != NULL, NULL);
-    g_return_val_if_fail (charset != MM_MODEM_CHARSET_UNKNOWN, NULL);
-
-    iconv_from = charset_iconv_from (charset);
-    g_return_val_if_fail (iconv_from != NULL, FALSE);
-
-    unconverted = mm_utils_hexstr2bin (src, &unconverted_len);
-    if (!unconverted)
-        return NULL;
-
-    if (charset == MM_MODEM_CHARSET_UTF8 || charset == MM_MODEM_CHARSET_IRA)
-        return g_steal_pointer (&unconverted);
-
-    converted = g_convert (unconverted, unconverted_len,
-                           "UTF-8//TRANSLIT", iconv_from,
-                           NULL, NULL, &error);
-    if (!converted || error)
-        return NULL;
-
-    return g_steal_pointer (&converted);
-}
-
-gchar *
-mm_modem_charset_utf8_to_hex (const gchar    *src,
-                              MMModemCharset  charset)
-{
-    const gchar      *iconv_to;
-    g_autofree gchar *converted = NULL;
-    g_autoptr(GError) error = NULL;
-    gsize             converted_len = 0;
-
-    g_return_val_if_fail (src != NULL, NULL);
-    g_return_val_if_fail (charset != MM_MODEM_CHARSET_UNKNOWN, NULL);
-
-    iconv_to = charset_iconv_from (charset);
-    g_return_val_if_fail (iconv_to != NULL, FALSE);
-
-    if (charset == MM_MODEM_CHARSET_UTF8 || charset == MM_MODEM_CHARSET_IRA)
-        return g_strdup (src);
-
-    converted = g_convert (src, strlen (src),
-                           iconv_to, "UTF-8//TRANSLIT",
-                           NULL, &converted_len, &error);
-    if (!converted || error)
-        return NULL;
-
-    /* Get hex representation of the string */
-    return mm_utils_bin2hexstr ((guint8 *)converted, converted_len);
-}
-
+/******************************************************************************/
 /* GSM 03.38 encoding conversion stuff */
 
 #define GSM_DEF_ALPHABET_SIZE 128
@@ -337,6 +209,22 @@
     return FALSE;
 }
 
+static gboolean
+translit_gsm_nul_byte (GByteArray *gsm)
+{
+    guint i;
+    guint n_replaces = 0;
+
+    for (i = 0; i < gsm->len; i++) {
+        if (gsm->data[i] == 0x00) {
+            utf8_to_gsm_def_char (translit_fallback, strlen (translit_fallback), &gsm->data[i]);
+            n_replaces++;
+        }
+    }
+
+    return (n_replaces > 0);
+}
+
 
 #define EONE(a, g)        { {a, 0x00, 0x00}, 1, g }
 #define ETHR(a, b, c, g)  { {a, b,    c},    3, g }
@@ -393,12 +281,14 @@
     return FALSE;
 }
 
-guint8 *
-mm_charset_gsm_unpacked_to_utf8 (const guint8 *gsm,
-                                 guint32       len)
+static guint8 *
+charset_gsm_unpacked_to_utf8 (const guint8  *gsm,
+                              guint32        len,
+                              gboolean       translit,
+                              GError       **error)
 {
-    guint       i;
-    GByteArray *utf8;
+    g_autoptr(GByteArray) utf8 = NULL;
+    guint                 i;
 
     g_return_val_if_fail (gsm != NULL, NULL);
     g_return_val_if_fail (len < 4096, NULL);
@@ -444,26 +334,36 @@
 
         if (ulen)
             g_byte_array_append (utf8, &uchars[0], ulen);
-        else
-            g_byte_array_append (utf8, (guint8 *) "?", 1);
+        else if (translit)
+            g_byte_array_append (utf8, (guint8 *) translit_fallback, strlen (translit_fallback));
+        else {
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                         "Invalid conversion from GSM7");
+            return NULL;
+        }
     }
 
     /* Always make sure returned string is NUL terminated */
     g_byte_array_append (utf8, (guint8 *) "\0", 1);
-    return g_byte_array_free (utf8, FALSE);
+    return g_byte_array_free (g_steal_pointer (&utf8), FALSE);
 }
 
-guint8 *
-mm_charset_utf8_to_unpacked_gsm (const gchar *utf8,
-                                 guint32     *out_len)
+static guint8 *
+charset_utf8_to_unpacked_gsm (const gchar  *utf8,
+                              gboolean      translit,
+                              guint32      *out_len,
+                              GError      **error)
 {
-    GByteArray          *gsm;
-    const gchar         *c;
-    const gchar         *next;
-    static const guint8  gesc = GSM_ESCAPE_CHAR;
+    g_autoptr(GByteArray)  gsm = NULL;
+    const gchar           *c;
+    const gchar           *next;
+    static const guint8    gesc = GSM_ESCAPE_CHAR;
 
-    g_return_val_if_fail (utf8 != NULL, NULL);
-    g_return_val_if_fail (g_utf8_validate (utf8, -1, NULL), NULL);
+    if (!utf8 || !g_utf8_validate (utf8, -1, NULL)) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Couldn't convert UTF-8 to GSM: input UTF-8 validation failed");
+        return NULL;
+    }
 
     /* worst case initial length */
     gsm = g_byte_array_sized_new (g_utf8_strlen (utf8, -1) * 2 + 1);
@@ -473,7 +373,7 @@
         g_byte_array_append (gsm, (guint8 *) "\0", 1);
         if (out_len)
             *out_len = 0;
-        return g_byte_array_free (gsm, FALSE);
+        return g_byte_array_free (g_steal_pointer (&gsm), FALSE);
     }
 
     next = utf8;
@@ -488,8 +388,16 @@
             /* Add the escape char */
             g_byte_array_append (gsm, &gesc, 1);
             g_byte_array_append (gsm, &gch, 1);
-        } else if (utf8_to_gsm_def_char (c, next - c, &gch))
+        } else if (utf8_to_gsm_def_char (c, next - c, &gch)) {
             g_byte_array_append (gsm, &gch, 1);
+        } else if (translit) {
+            /* add ? */
+            g_byte_array_append (gsm, &gch, 1);
+        } else {
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                         "Couldn't convert UTF-8 char to GSM");
+            return NULL;
+        }
 
         c = next;
     }
@@ -500,9 +408,13 @@
 
     /* Always make sure returned string is NUL terminated */
     g_byte_array_append (gsm, (guint8 *) "\0", 1);
-    return g_byte_array_free (gsm, FALSE);
+    return g_byte_array_free (g_steal_pointer (&gsm), FALSE);
 }
 
+/******************************************************************************/
+/* Checks to see whether conversion to a target charset may be done without
+ * any loss. */
+
 static gboolean
 gsm_is_subset (gunichar     c,
                const gchar *utf8,
@@ -632,13 +544,6 @@
     { MM_MODEM_CHARSET_PCDN,    pcdn_is_subset     },
 };
 
-/**
- * mm_charset_can_covert_to:
- * @utf8: UTF-8 valid string.
- * @charset: the #MMModemCharset to validate the conversion from @utf8.
- *
- * Returns: %TRUE if the conversion is possible without errors, %FALSE otherwise.
- */
 gboolean
 mm_charset_can_convert_to (const gchar    *utf8,
                            MMModemCharset  charset)
@@ -682,6 +587,9 @@
     return TRUE;
 }
 
+/******************************************************************************/
+/* GSM-7 pack/unpack operations */
+
 guint8 *
 mm_charset_gsm_unpack (const guint8 *gsm,
                        guint32       num_septets,
@@ -754,217 +662,308 @@
     return packed;
 }
 
-/* We do all our best to get the given string, which is possibly given in the
- * specified charset, to UTF8. It may happen that the given string is really
- * the hex representation of the charset-encoded string, so we need to cope with
- * that case. */
-gchar *
-mm_charset_take_and_convert_to_utf8 (gchar          *str,
-                                     MMModemCharset  charset)
+/*****************************************************************************/
+/* Main conversion functions */
+
+static guint8 *
+charset_iconv_from_utf8 (const gchar            *utf8,
+                         const CharsetSettings  *settings,
+                         gboolean                translit,
+                         guint                  *out_size,
+                         GError                **error)
 {
-    gchar *utf8 = NULL;
+    g_autoptr(GError)      inner_error = NULL;
+    gsize                  bytes_written = 0;
+    g_autofree guint8     *encoded = NULL;
 
-    if (!str)
+    encoded = (guint8 *) g_convert (utf8, -1,
+                                    settings->iconv_name, "UTF-8",
+                                    NULL, &bytes_written, &inner_error);
+    if (encoded) {
+        if (out_size)
+            *out_size = (guint) bytes_written;
+        return g_steal_pointer (&encoded);
+    }
+
+    if (!translit) {
+        g_propagate_error (error, g_steal_pointer (&inner_error));
+        g_prefix_error (error, "Couldn't convert from UTF-8 to %s: ", settings->gsm_name);
         return NULL;
-
-    switch (charset) {
-    case MM_MODEM_CHARSET_UNKNOWN:
-        g_warn_if_reached ();
-        utf8 = str;
-        break;
-
-    case MM_MODEM_CHARSET_HEX:
-        /* We'll assume that the HEX string is really valid ASCII at the end */
-        utf8 = str;
-        break;
-
-    case MM_MODEM_CHARSET_GSM:
-        utf8 = (gchar *) mm_charset_gsm_unpacked_to_utf8 ((const guint8 *) str, strlen (str));
-        g_free (str);
-        break;
-
-    case MM_MODEM_CHARSET_8859_1:
-    case MM_MODEM_CHARSET_PCCP437:
-    case MM_MODEM_CHARSET_PCDN: {
-        const gchar *iconv_from;
-        GError *error = NULL;
-
-        iconv_from = charset_iconv_from (charset);
-        utf8 = g_convert (str, strlen (str),
-                          "UTF-8//TRANSLIT", iconv_from,
-                          NULL, NULL, &error);
-        if (!utf8 || error) {
-            g_clear_error (&error);
-            utf8 = NULL;
-        }
-
-        g_free (str);
-        break;
     }
 
-    case MM_MODEM_CHARSET_UCS2:
-    case MM_MODEM_CHARSET_UTF16: {
-        gsize len;
-        gboolean possibly_hex = TRUE;
-        gsize bread = 0, bwritten = 0;
-
-        /* If the string comes in hex-UCS-2, len needs to be a multiple of 4 */
-        len = strlen (str);
-        if ((len < 4) || ((len % 4) != 0))
-            possibly_hex = FALSE;
-        else {
-            const gchar *p = str;
-
-            /* All chars in the string must be hex */
-            while (*p && possibly_hex)
-                possibly_hex = isxdigit (*p++);
-        }
-
-        /* If hex, then we expect hex-encoded UCS-2 */
-        if (possibly_hex) {
-            utf8 = mm_modem_charset_hex_to_utf8 (str, charset);
-            if (utf8) {
-                g_free (str);
-                break;
-            }
-        }
-
-        /* If not hex, then it might be raw UCS-2 (very unlikely) or ASCII/UTF-8
-         * (much more likely).  Try to convert to UTF-8 and if that fails, use
-         * the partial conversion length to re-convert the part of the string
-         * that is UTF-8, if any.
-         */
-        utf8 = g_convert (str, strlen (str),
-                          "UTF-8//TRANSLIT", "UTF-8//TRANSLIT",
-                          &bread, &bwritten, NULL);
-
-        /* Valid conversion, or we didn't get enough valid UTF-8 */
-        if (utf8 || (bwritten <= 2)) {
-            g_free (str);
-            break;
-        }
-
-        /* Last try; chop off the original string at the conversion failure
-         * location and get what we can.
-         */
-        str[bread] = '\0';
-        utf8 = g_convert (str, strlen (str),
-                          "UTF-8//TRANSLIT", "UTF-8//TRANSLIT",
-                          NULL, NULL, NULL);
-        g_free (str);
-        break;
+    encoded = (guint8 *) g_convert_with_fallback (utf8, -1,
+                                                  settings->iconv_name, "UTF-8", translit_fallback,
+                                                  NULL, &bytes_written, error);
+    if (encoded) {
+        if (out_size)
+            *out_size = (guint) bytes_written;
+        return g_steal_pointer (&encoded);
     }
 
-    /* If the given charset is ASCII or UTF8, we really expect the final string
-     * already here */
-    case MM_MODEM_CHARSET_IRA:
-    case MM_MODEM_CHARSET_UTF8:
-        utf8 = str;
-        break;
-
-    default:
-        g_assert_not_reached ();
-    }
-
-    /* Validate UTF-8 always before returning. This result will be exposed in DBus
-     * very likely... */
-    if (utf8 && !g_utf8_validate (utf8, -1, NULL)) {
-        /* Better return NULL than an invalid UTF-8 string */
-        g_free (utf8);
-        utf8 = NULL;
-    }
-
-    return utf8;
+    g_prefix_error (error, "Couldn't convert from UTF-8 to %s with translit: ", settings->gsm_name);
+    return NULL;
 }
 
-/* We do all our best to convert the given string, which comes in UTF-8, to the
- * specified charset. It may be that the output string needs to be the hex
- * representation of the charset-encoded string, so we need to cope with that
- * case. */
-gchar *
-mm_utf8_take_and_convert_to_charset (gchar          *str,
-                                     MMModemCharset  charset)
+GByteArray *
+mm_modem_charset_bytearray_from_utf8 (const gchar     *utf8,
+                                      MMModemCharset   charset,
+                                      gboolean         translit,
+                                      GError         **error)
 {
-    gchar *encoded = NULL;
+    const CharsetSettings *settings;
+    guint8                *encoded = NULL;
+    guint                  encoded_size = 0;
 
-    if (!str)
-        return NULL;
+    settings = lookup_charset_settings (charset);
 
-    /* Validate UTF-8 always before converting */
-    if (!g_utf8_validate (str, -1, NULL)) {
-        /* Better return NULL than an invalid encoded string */
-        g_free (str);
+    if (charset == MM_MODEM_CHARSET_UNKNOWN) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Cannot convert from UTF-8: unknown target charset");
         return NULL;
     }
 
     switch (charset) {
-    case MM_MODEM_CHARSET_UNKNOWN:
-        g_warn_if_reached ();
-        encoded = str;
-        break;
+        case MM_MODEM_CHARSET_GSM:
+            encoded = charset_utf8_to_unpacked_gsm (utf8, translit, &encoded_size, error);
+            break;
+        case MM_MODEM_CHARSET_IRA:
+        case MM_MODEM_CHARSET_8859_1:
+        case MM_MODEM_CHARSET_UTF8:
+        case MM_MODEM_CHARSET_UCS2:
+        case MM_MODEM_CHARSET_PCCP437:
+        case MM_MODEM_CHARSET_PCDN:
+        case MM_MODEM_CHARSET_UTF16:
+            encoded = charset_iconv_from_utf8 (utf8, settings, translit, &encoded_size, error);
+            break;
+        case MM_MODEM_CHARSET_UNKNOWN:
+        default:
+            g_assert_not_reached ();
+    }
 
-    case MM_MODEM_CHARSET_HEX:
-        encoded = str;
-        break;
+    return g_byte_array_new_take (encoded, encoded_size);
+}
 
-    case MM_MODEM_CHARSET_GSM:
-        encoded = (gchar *) mm_charset_utf8_to_unpacked_gsm (str, NULL);
-        g_free (str);
-        break;
+gchar *
+mm_modem_charset_str_from_utf8 (const gchar     *utf8,
+                                MMModemCharset   charset,
+                                gboolean         translit,
+                                GError         **error)
+{
+    g_autoptr(GByteArray) bytearray = NULL;
 
-    case MM_MODEM_CHARSET_8859_1:
-    case MM_MODEM_CHARSET_PCCP437:
-    case MM_MODEM_CHARSET_PCDN: {
-        const gchar *iconv_to;
-        GError *error = NULL;
+    if (charset == MM_MODEM_CHARSET_UNKNOWN) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Cannot convert from UTF-8: unknown target charset");
+        return NULL;
+    }
 
-        iconv_to = charset_iconv_from (charset);
-        encoded = g_convert (str, strlen (str),
-                             iconv_to, "UTF-8",
-                             NULL, NULL, &error);
-        if (!encoded || error) {
-            g_clear_error (&error);
-            encoded = NULL;
+    bytearray = mm_modem_charset_bytearray_from_utf8 (utf8, charset, translit, error);
+    if (!bytearray)
+        return NULL;
+
+    switch (charset) {
+        case MM_MODEM_CHARSET_GSM:
+            /* Note: strings encoded in unpacked GSM-7 can be used as plain
+             * strings as long as the string doesn't contain character '@', which
+             * is the one encoded as 0x00. At this point, we perform transliteration
+             * of the NUL bytes in the GSM-7 bytearray, and we fail the operation
+             * if one or more replacements were done and transliteration wasn't
+             * requested */
+            if (translit_gsm_nul_byte (bytearray) && !translit) {
+                g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                             "Cannot convert to GSM-7 string: transliteration required for embedded '@'");
+                return NULL;
+            }
+            /* fall through */
+        case MM_MODEM_CHARSET_IRA:
+        case MM_MODEM_CHARSET_8859_1:
+        case MM_MODEM_CHARSET_UTF8:
+        case MM_MODEM_CHARSET_PCCP437:
+        case MM_MODEM_CHARSET_PCDN:
+            return (gchar *) g_byte_array_free (g_steal_pointer (&bytearray), FALSE);
+        case MM_MODEM_CHARSET_UCS2:
+        case MM_MODEM_CHARSET_UTF16:
+            return mm_utils_bin2hexstr (bytearray->data, bytearray->len);
+        default:
+        case MM_MODEM_CHARSET_UNKNOWN:
+            g_assert_not_reached ();
+    }
+}
+
+static gchar *
+charset_iconv_to_utf8 (const guint8           *data,
+                       guint32                 len,
+                       const CharsetSettings  *settings,
+                       gboolean                translit,
+                       GError                **error)
+{
+    g_autoptr(GError)  inner_error = NULL;
+    g_autofree gchar  *utf8 = NULL;
+
+    utf8 = g_convert ((const gchar *) data, len,
+                      "UTF-8",
+                      settings->iconv_name,
+                      NULL, NULL, &inner_error);
+    if (utf8)
+        return g_steal_pointer (&utf8);
+
+    if (!translit) {
+        g_propagate_error (error, g_steal_pointer (&inner_error));
+        g_prefix_error (error, "Couldn't convert from %s to UTF-8: ", settings->gsm_name);
+        return NULL;
+    }
+
+    utf8 = g_convert_with_fallback ((const gchar *) data, len,
+                                    "UTF-8", settings->iconv_name, translit_fallback,
+                                    NULL, NULL, error);
+    if (utf8)
+        return g_steal_pointer (&utf8);
+
+    g_prefix_error (error, "Couldn't convert from %s to UTF-8 with translit: ", settings->gsm_name);
+    return NULL;
+}
+
+gchar *
+mm_modem_charset_bytearray_to_utf8 (GByteArray      *bytearray,
+                                    MMModemCharset   charset,
+                                    gboolean         translit,
+                                    GError         **error)
+{
+    const CharsetSettings *settings;
+    g_autofree gchar      *utf8 = NULL;
+
+    if (charset == MM_MODEM_CHARSET_UNKNOWN) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Cannot convert from UTF-8: unknown target charset");
+        return NULL;
+    }
+
+    settings = lookup_charset_settings (charset);
+
+    switch (charset) {
+        case MM_MODEM_CHARSET_GSM:
+            utf8 = (gchar *) charset_gsm_unpacked_to_utf8 (bytearray->data,
+                                                           bytearray->len,
+                                                           translit,
+                                                           error);
+            break;
+        case MM_MODEM_CHARSET_IRA:
+        case MM_MODEM_CHARSET_UTF8:
+        case MM_MODEM_CHARSET_8859_1:
+        case MM_MODEM_CHARSET_PCCP437:
+        case MM_MODEM_CHARSET_PCDN:
+        case MM_MODEM_CHARSET_UCS2:
+        case MM_MODEM_CHARSET_UTF16:
+            utf8 = charset_iconv_to_utf8 (bytearray->data,
+                                          bytearray->len,
+                                          settings,
+                                          translit,
+                                          error);
+            break;
+        case MM_MODEM_CHARSET_UNKNOWN:
+        default:
+            g_assert_not_reached ();
+    }
+
+    if (utf8 && g_utf8_validate (utf8, -1, NULL))
+        return g_steal_pointer (&utf8);
+
+    g_prefix_error (error, "Invalid conversion from %s to UTF-8: ", settings->gsm_name);
+    return NULL;
+}
+
+gchar *
+mm_modem_charset_str_to_utf8 (const gchar     *str,
+                              gssize           len,
+                              MMModemCharset   charset,
+                              gboolean         translit,
+                              GError         **error)
+{
+    g_autoptr(GByteArray) bytearray = NULL;
+
+    if (charset == MM_MODEM_CHARSET_UNKNOWN) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
+                     "Cannot convert from UTF-8: unknown target charset");
+        return NULL;
+    }
+
+    /* Note: if the input string is GSM-7 encoded and it contains the '@'
+     * character, using -1 to indicate string length won't work properly,
+     * as '@' is encoded as 0x00. Whenever possible, if using GSM-7,
+     * give a proper len value or otherwise use the bytearray_to_utf8()
+     * method instead. */
+    if (len < 0)
+        len = strlen (str);
+
+    switch (charset) {
+        case MM_MODEM_CHARSET_GSM:
+        case MM_MODEM_CHARSET_IRA:
+        case MM_MODEM_CHARSET_8859_1:
+        case MM_MODEM_CHARSET_UTF8:
+        case MM_MODEM_CHARSET_PCCP437:
+        case MM_MODEM_CHARSET_PCDN:
+            bytearray = g_byte_array_sized_new (len);
+            g_byte_array_append (bytearray, (const guint8 *)str, len);
+            break;
+        case MM_MODEM_CHARSET_UCS2:
+        case MM_MODEM_CHARSET_UTF16: {
+            guint8 *bin = NULL;
+            gsize   bin_len;
+
+            bin = (guint8 *) mm_utils_hexstr2bin (str, len, &bin_len, error);
+            if (!bin)
+                return NULL;
+
+            bytearray = g_byte_array_new_take (bin, bin_len);
+            break;
+        }
+        case MM_MODEM_CHARSET_UNKNOWN:
+        default:
+            g_assert_not_reached ();
+    }
+
+    return mm_modem_charset_bytearray_to_utf8 (bytearray, charset, translit, error);
+}
+
+/******************************************************************************/
+/* Runtime charset support via iconv() */
+
+void
+mm_modem_charsets_init (void)
+{
+    /* As test string, something we can convert to/from all the encodings */
+    static const gchar *default_test_str = "ModemManager";
+    guint               i;
+
+    mm_obj_dbg (NULL, "[charsets] detecting platform iconv() support...");
+    for (i = 0; i < G_N_ELEMENTS (charset_settings); i++) {
+        g_autofree guint8 *enc = NULL;
+        guint              enc_size;
+        g_autofree gchar  *dec = NULL;
+
+        if (!charset_settings[i].iconv_name)
+            continue;
+
+        enc = charset_iconv_from_utf8 (default_test_str,
+                                       &charset_settings[i],
+                                       FALSE,
+                                       &enc_size,
+                                       NULL);
+        if (!enc) {
+            mm_obj_dbg (NULL, "[charsets]   %s: iconv conversion to charset not supported", charset_settings[i].iconv_name);
+            continue;
         }
 
-        g_free (str);
-        break;
-    }
-
-    case MM_MODEM_CHARSET_UCS2:
-    case MM_MODEM_CHARSET_UTF16: {
-        const gchar *iconv_to;
-        gsize encoded_len = 0;
-        GError *error = NULL;
-        gchar *hex;
-
-        iconv_to = charset_iconv_from (charset);
-        encoded = g_convert (str, strlen (str),
-                             iconv_to, "UTF-8",
-                             NULL, &encoded_len, &error);
-        if (!encoded || error) {
-            g_clear_error (&error);
-            encoded = NULL;
+        dec = charset_iconv_to_utf8 (enc,
+                                     enc_size,
+                                     &charset_settings[i],
+                                     FALSE,
+                                     NULL);
+        if (!enc) {
+            mm_obj_dbg (NULL, "[charsets]   %s: iconv conversion from charset not supported", charset_settings[i].iconv_name);
+            continue;
         }
 
-        /* Get hex representation of the string */
-        hex = mm_utils_bin2hexstr ((guint8 *)encoded, encoded_len);
-        g_free (encoded);
-        encoded = hex;
-        g_free (str);
-        break;
+        mm_obj_dbg (NULL, "[charsets]   %s: iconv conversion to/from charset is supported", charset_settings[i].iconv_name);
     }
-
-    /* If the given charset is ASCII or UTF8, we really expect the final string
-     * already here. */
-    case MM_MODEM_CHARSET_IRA:
-    case MM_MODEM_CHARSET_UTF8:
-        encoded = str;
-        break;
-
-    default:
-        g_assert_not_reached ();
-    }
-
-    return encoded;
 }
diff --git a/src/mm-charsets.h b/src/mm-charsets.h
index c064eef..3071f6b 100644
--- a/src/mm-charsets.h
+++ b/src/mm-charsets.h
@@ -18,53 +18,24 @@
 
 #include <glib.h>
 
+/*****************************************************************************************/
+
 typedef enum {
-    MM_MODEM_CHARSET_UNKNOWN = 0x00000000,
-    MM_MODEM_CHARSET_GSM     = 0x00000001,
-    MM_MODEM_CHARSET_IRA     = 0x00000002,
-    MM_MODEM_CHARSET_8859_1  = 0x00000004,
-    MM_MODEM_CHARSET_UTF8    = 0x00000008,
-    MM_MODEM_CHARSET_UCS2    = 0x00000010,
-    MM_MODEM_CHARSET_PCCP437 = 0x00000020,
-    MM_MODEM_CHARSET_PCDN    = 0x00000040,
-    MM_MODEM_CHARSET_HEX     = 0x00000080,
-    MM_MODEM_CHARSET_UTF16   = 0x00000100,
+    MM_MODEM_CHARSET_UNKNOWN = 0,
+    MM_MODEM_CHARSET_GSM     = 1 << 0,
+    MM_MODEM_CHARSET_IRA     = 1 << 1,
+    MM_MODEM_CHARSET_8859_1  = 1 << 2,
+    MM_MODEM_CHARSET_UTF8    = 1 << 3,
+    MM_MODEM_CHARSET_UCS2    = 1 << 4,
+    MM_MODEM_CHARSET_PCCP437 = 1 << 5,
+    MM_MODEM_CHARSET_PCDN    = 1 << 6,
+    MM_MODEM_CHARSET_UTF16   = 1 << 7,
 } MMModemCharset;
 
 const gchar    *mm_modem_charset_to_string   (MMModemCharset  charset);
 MMModemCharset  mm_modem_charset_from_string (const gchar    *string);
 
-/* Append the given string to the given byte array but re-encode it
- * into the given charset first.  The original string is assumed to be
- * UTF-8 encoded.
- */
-gboolean mm_modem_charset_byte_array_append (GByteArray      *array,
-                                             const gchar     *utf8,
-                                             gboolean         quoted,
-                                             MMModemCharset   charset,
-                                             GError         **error);
-
-/* Take a string encoded in the given charset in binary form, and
- * convert it to UTF-8. */
-gchar *mm_modem_charset_byte_array_to_utf8 (GByteArray     *array,
-                                            MMModemCharset  charset);
-
-/* Take a string in hex representation ("00430052" or "A4BE11" for example)
- * and convert it from the given character set to UTF-8.
- */
-gchar *mm_modem_charset_hex_to_utf8 (const gchar    *src,
-                                    MMModemCharset  charset);
-
-/* Take a string in UTF-8 and convert it to the given charset in hex
- * representation.
- */
-gchar *mm_modem_charset_utf8_to_hex (const gchar    *src,
-                                     MMModemCharset  charset);
-
-guint8 *mm_charset_utf8_to_unpacked_gsm (const gchar  *utf8,
-                                         guint32      *out_len);
-guint8 *mm_charset_gsm_unpacked_to_utf8 (const guint8 *gsm,
-                                         guint32       len);
+/*****************************************************************************************/
 
 /* Checks whether conversion to the given charset may be done without errors */
 gboolean mm_charset_can_convert_to (const gchar    *utf8,
@@ -80,9 +51,65 @@
                              guint8        start_offset,  /* in bits */
                              guint32      *out_packed_len);
 
-gchar *mm_charset_take_and_convert_to_utf8 (gchar          *str,
-                                            MMModemCharset  charset);
-gchar *mm_utf8_take_and_convert_to_charset (gchar          *str,
-                                            MMModemCharset  charset);
+/*****************************************************************************************/
+
+/*
+ * Convert the given UTF-8 encoded string into the given charset.
+ *
+ * The output is given as a bytearray, because the target charset may allow
+ * embedded NUL bytes (e.g. UTF-16).
+ *
+ * The output encoded string is not guaranteed to be NUL-terminated, instead
+ * the bytearray length itself gives the correct string length.
+ */
+GByteArray *mm_modem_charset_bytearray_from_utf8 (const gchar     *utf8,
+                                                  MMModemCharset   charset,
+                                                  gboolean         translit,
+                                                  GError         **error);
+
+/*
+ * Convert the given UTF-8 encoded string into the given charset.
+ *
+ * The output is given as a C string, and those charsets that allow
+ * embedded NUL bytes (e.g. UTF-16) will be hex-encoded.
+ *
+ * The output encoded string is guaranteed to be NUL-terminated, and so no
+ * explicit output length is returned.
+ */
+gchar *mm_modem_charset_str_from_utf8 (const gchar     *utf8,
+                                       MMModemCharset   charset,
+                                       gboolean         translit,
+                                       GError         **error);
+
+/*
+ * Convert into an UTF-8 encoded string the input byte array, which is
+ * encoded in the given charset.
+ *
+ * The output string is guaranteed to be valid UTF-8 and NUL-terminated.
+ */
+gchar *mm_modem_charset_bytearray_to_utf8 (GByteArray      *bytearray,
+                                           MMModemCharset   charset,
+                                           gboolean         translit,
+                                           GError         **error);
+
+/*
+ * Convert into an UTF-8 encoded string the input string, which is
+ * encoded in the given charset. Those charsets that allow embedded NUL
+ * bytes (e.g. UTF-16) need to be hex-encoded.
+ *
+ * If the input string is NUL-terminated, len may be given as -1; otherwise
+ * len needs to specify the number of valid bytes in the input string.
+ *
+ * The output string is guaranteed to be valid UTF-8 and NUL-terminated.
+ */
+gchar *mm_modem_charset_str_to_utf8 (const gchar     *str,
+                                     gssize           len,
+                                     MMModemCharset   charset,
+                                     gboolean         translit,
+                                     GError         **error);
+
+/*****************************************************************************************/
+
+void mm_modem_charsets_init (void);
 
 #endif /* MM_CHARSETS_H */
diff --git a/src/mm-modem-helpers-mbim.c b/src/mm-modem-helpers-mbim.c
index f9dc8e6..b70f379 100644
--- a/src/mm-modem-helpers-mbim.c
+++ b/src/mm-modem-helpers-mbim.c
@@ -19,6 +19,8 @@
 #include "mm-errors-types.h"
 #include "mm-log-object.h"
 
+#include <string.h>
+
 /*****************************************************************************/
 
 MMModemCapability
diff --git a/src/mm-modem-helpers-qmi.c b/src/mm-modem-helpers-qmi.c
index 012e657..6ab0adf 100644
--- a/src/mm-modem-helpers-qmi.c
+++ b/src/mm-modem-helpers-qmi.c
@@ -1845,25 +1845,19 @@
 
     /* The length will be exactly EXPECTED_QMI_UNIQUE_ID_LENGTH*2 if given in HEX */
     if (len == (2 * EXPECTED_QMI_UNIQUE_ID_LENGTH)) {
-        guint8 *tmp;
-        gsize   tmp_len;
-        guint   i;
-
-        for (i = 0; i < len; i++) {
-            if (!g_ascii_isxdigit (unique_id[i])) {
-                g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                             "Unexpected character found in unique id (not HEX): %c", unique_id[i]);
-                return NULL;
-            }
-        }
+        g_autofree guint8 *tmp = NULL;
+        gsize              tmp_len;
 
         tmp_len = 0;
-        tmp = (guint8 *) mm_utils_hexstr2bin (unique_id, &tmp_len);
+        tmp = mm_utils_hexstr2bin (unique_id, -1, &tmp_len, error);
+        if (!tmp) {
+            g_prefix_error (error, "Unexpected character found in unique id: ");
+            return NULL;
+        }
         g_assert (tmp_len == EXPECTED_QMI_UNIQUE_ID_LENGTH);
 
         qmi_unique_id = g_array_sized_new (FALSE, FALSE, sizeof (guint8), tmp_len);
         g_array_insert_vals (qmi_unique_id, 0, tmp, tmp_len);
-        g_free (tmp);
         return qmi_unique_id;
     }
 
diff --git a/src/mm-modem-helpers.c b/src/mm-modem-helpers.c
index e5776fc..7ff7484 100644
--- a/src/mm-modem-helpers.c
+++ b/src/mm-modem-helpers.c
@@ -1289,9 +1289,9 @@
         info->operator_code = mm_get_string_unquoted_from_match_info (match_info, 4);
 
         /* The returned strings may be given in e.g. UCS2 */
-        mm_3gpp_normalize_operator (&info->operator_long,  cur_charset);
-        mm_3gpp_normalize_operator (&info->operator_short, cur_charset);
-        mm_3gpp_normalize_operator (&info->operator_code,  cur_charset);
+        mm_3gpp_normalize_operator (&info->operator_long,  cur_charset, log_object);
+        mm_3gpp_normalize_operator (&info->operator_short, cur_charset, log_object);
+        mm_3gpp_normalize_operator (&info->operator_code,  cur_charset, log_object);
 
         /* Only try for access technology with UMTS-format matches.
          * If none give, assume GSM */
@@ -4053,8 +4053,11 @@
 
 void
 mm_3gpp_normalize_operator (gchar          **operator,
-                            MMModemCharset   cur_charset)
+                            MMModemCharset   cur_charset,
+                            gpointer         log_object)
 {
+    g_autofree gchar *normalized = NULL;
+
     g_assert (operator);
 
     if (*operator == NULL)
@@ -4062,31 +4065,38 @@
 
     /* Despite +CSCS? may claim supporting UCS2, Some modems (e.g. Huawei)
      * always report the operator name in ASCII in a +COPS response. */
-    if (cur_charset == MM_MODEM_CHARSET_UCS2) {
-        gchar *tmp;
+    if (cur_charset != MM_MODEM_CHARSET_UNKNOWN) {
+        g_autoptr(GError) error = NULL;
 
-        tmp = g_strdup (*operator);
-        /* In this case we're already checking UTF-8 validity */
-        tmp = mm_charset_take_and_convert_to_utf8 (tmp, cur_charset);
-        if (tmp) {
-            g_clear_pointer (operator, g_free);
-            *operator = tmp;
+        normalized = mm_modem_charset_str_to_utf8 (*operator, -1, cur_charset, TRUE, &error);
+        if (normalized)
             goto out;
-        }
+
+        mm_obj_dbg (log_object, "couldn't convert operator string '%s' from charset '%s': %s",
+                    *operator,
+                    mm_modem_charset_to_string (cur_charset),
+                    error->message);
     }
 
     /* Charset is unknown or there was an error in conversion; try to see
      * if the contents we got are valid UTF-8 already. */
-    if (!g_utf8_validate (*operator, -1, NULL))
-        g_clear_pointer (operator, g_free);
+    if (g_utf8_validate (*operator, -1, NULL))
+        normalized = g_strdup (*operator);
 
 out:
 
     /* Some modems (Novatel LTE) return the operator name as "Unknown" when
      * it fails to obtain the operator name. Return NULL in such case.
      */
-    if (*operator && g_ascii_strcasecmp (*operator, "unknown") == 0)
+    if (!normalized || g_ascii_strcasecmp (normalized, "unknown") == 0) {
+        /* If normalization failed, just cleanup the string */
         g_clear_pointer (operator, g_free);
+        return;
+    }
+
+    mm_obj_dbg (log_object, "operator normalized '%s'->'%s'", *operator, normalized);
+    g_clear_pointer (operator, g_free);
+    *operator = g_steal_pointer (&normalized);
 }
 
 /*************************************************************************/
@@ -4326,10 +4336,9 @@
         return NULL;
     }
 
-    bin = (guint8 *) mm_utils_hexstr2bin (raw, &binlen);
+    bin = mm_utils_hexstr2bin (raw, -1, &binlen, error);
     if (!bin) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                     "invalid raw emergency numbers list contents: %s", raw);
+        g_prefix_error (error, "invalid raw emergency numbers list contents: ");
         return NULL;
     }
 
@@ -5152,3 +5161,87 @@
     g_strfreev (split);
     return valid;
 }
+
+/*****************************************************************************/
+
+gboolean
+mm_sim_parse_cpol_query_response (const gchar  *response,
+                                  gchar       **out_operator_code,
+                                  gboolean     *out_gsm_act,
+                                  gboolean     *out_gsm_compact_act,
+                                  gboolean     *out_utran_act,
+                                  gboolean     *out_eutran_act,
+                                  gboolean     *out_ngran_act,
+                                  GError      **error)
+{
+    g_autoptr(GMatchInfo)  match_info = NULL;
+    g_autoptr(GRegex)      r = NULL;
+    g_autofree gchar      *operator_code = NULL;
+    guint                  format = 0;
+    guint                  act = 0;
+    guint                  match_count;
+
+    r = g_regex_new ("\\+CPOL:\\s*\\d+,\\s*(\\d+),\\s*\"(\\d+)\""
+                     "(?:,\\s*(\\d+))?"     /* GSM_AcTn */
+                     "(?:,\\s*(\\d+))?"     /* GSM_Compact_AcTn */
+                     "(?:,\\s*(\\d+))?"     /* UTRAN_AcTn */
+                     "(?:,\\s*(\\d+))?"     /* E-UTRAN_AcTn */
+                     "(?:,\\s*(\\d+))?",    /* NG-RAN_AcTn */
+                     G_REGEX_RAW, 0, NULL);
+    g_regex_match (r, response, 0, &match_info);
+
+    if (!g_match_info_matches (match_info)) {
+        g_set_error (error,
+                     MM_CORE_ERROR,
+                     MM_CORE_ERROR_FAILED,
+                     "Couldn't parse +CPOL reply: %s", response);
+        return FALSE;
+    }
+
+    match_count = g_match_info_get_match_count (match_info);
+    /* Remember that g_match_info_get_match_count() includes match #0 */
+    g_assert (match_count >= 3);
+
+    if (!mm_get_uint_from_match_info (match_info, 1, &format) ||
+        !(operator_code = mm_get_string_unquoted_from_match_info (match_info, 2))) {
+        g_set_error (error,
+                     MM_CORE_ERROR,
+                     MM_CORE_ERROR_FAILED,
+                     "Couldn't parse +CPOL reply parameters: %s", response);
+        return FALSE;
+    }
+
+    if (format != 2) {
+        g_set_error (error,
+                     MM_CORE_ERROR,
+                     MM_CORE_ERROR_FAILED,
+                     "+CPOL reply not using numeric operator code: %s", response);
+        return FALSE;
+    }
+
+    if (out_operator_code) {
+        *out_operator_code = g_steal_pointer (&operator_code);
+    }
+    if (out_gsm_act)
+        *out_gsm_act = match_count >= 4 &&
+                       mm_get_uint_from_match_info (match_info, 3, &act) &&
+                       act != 0;
+    if (out_gsm_compact_act)
+        *out_gsm_compact_act = match_count >= 5 &&
+                               mm_get_uint_from_match_info (match_info, 4, &act) &&
+                               act != 0;
+    if (out_utran_act)
+        *out_utran_act = match_count >= 6 &&
+                         mm_get_uint_from_match_info (match_info, 5, &act) &&
+                         act != 0;
+    if (out_eutran_act)
+        *out_eutran_act = match_count >= 7 &&
+                          mm_get_uint_from_match_info (match_info, 6, &act) &&
+                          act != 0;
+    if (out_ngran_act)
+        *out_ngran_act = match_count >= 8 &&
+                         mm_get_uint_from_match_info (match_info, 7, &act) &&
+                         act != 0;
+
+    return TRUE;
+}
diff --git a/src/mm-modem-helpers.h b/src/mm-modem-helpers.h
index b3b5138..785fd53 100644
--- a/src/mm-modem-helpers.h
+++ b/src/mm-modem-helpers.h
@@ -439,7 +439,8 @@
 MMModemAccessTechnology mm_string_to_access_tech (const gchar *string);
 
 void mm_3gpp_normalize_operator (gchar          **operator,
-                                 MMModemCharset   cur_charset);
+                                 MMModemCharset   cur_charset,
+                                 gpointer         log_object);
 
 gboolean mm_3gpp_parse_operator_id (const gchar *operator_id,
                                     guint16 *mcc,
@@ -530,6 +531,20 @@
                                 GError      **error);
 
 /*****************************************************************************/
+/* SIM specific helpers and utilities */
+/*****************************************************************************/
+
+/* +CPOL? response parser (for a single entry) - accepts only numeric operator format*/
+gboolean mm_sim_parse_cpol_query_response (const gchar  *response,
+                                           gchar       **out_operator_code,
+                                           gboolean     *out_gsm_act,
+                                           gboolean     *out_gsm_compact_act,
+                                           gboolean     *out_utran_act,
+                                           gboolean     *out_eutran_act,
+                                           gboolean     *out_ngran_act,
+                                           GError      **error);
+
+/*****************************************************************************/
 
 /* Useful when clamp-ing an unsigned integer with implicit low limit set to 0,
  * and in order to avoid -Wtype-limits warnings. */
diff --git a/src/mm-plugin-manager.c b/src/mm-plugin-manager.c
index 6c9bdf1..4e9f300 100644
--- a/src/mm-plugin-manager.c
+++ b/src/mm-plugin-manager.c
@@ -28,6 +28,7 @@
 #include "mm-plugin-manager.h"
 #include "mm-plugin.h"
 #include "mm-shared.h"
+#include "mm-utils.h"
 #include "mm-log-object.h"
 
 #define SHARED_PREFIX "libmm-shared"
diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c
index d3ff0c2..cd969c3 100644
--- a/src/mm-shared-qmi.c
+++ b/src/mm-shared-qmi.c
@@ -212,7 +212,7 @@
     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_task_return_error_if_cancelled (task);
     g_object_unref (task);
 }
 
@@ -3188,11 +3188,45 @@
     guint         current_slot_number;
     guint         active_slot_number;
     guint         active_logical_id;
+    GArray       *initial_slot_status;
+    GArray       *final_slot_status;
+    gulong        final_slot_status_timeout_id;
+    gulong        load_sim_slots_indication_id;
 } LoadSimSlotsContext;
 
 static void
+clear_load_sim_slot_callbacks (GTask *task)
+{
+    MMIfaceModem        *self;
+    LoadSimSlotsContext *ctx;
+    Private             *priv;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+    priv = get_private (MM_SHARED_QMI (self));
+
+    if (ctx->client_uim && ctx->load_sim_slots_indication_id) {
+        g_signal_handler_disconnect(ctx->client_uim, ctx->load_sim_slots_indication_id);
+        ctx->load_sim_slots_indication_id = 0;
+    }
+    if (ctx->final_slot_status_timeout_id) {
+        g_source_remove (ctx->final_slot_status_timeout_id);
+        ctx->final_slot_status_timeout_id = 0;
+    }
+    /* Restore hot swap detection because we aren't loading slots anymore */
+    if (priv->uim_slot_status_indication_id)
+        g_signal_handler_unblock (ctx->client_uim, priv->uim_slot_status_indication_id);
+}
+
+static void
 load_sim_slots_context_free (LoadSimSlotsContext *ctx)
 {
+    if (ctx->initial_slot_status)
+        g_array_unref (ctx->initial_slot_status);
+    if (ctx->final_slot_status)
+        g_array_unref (ctx->final_slot_status);
+    g_assert (ctx->load_sim_slots_indication_id == 0);
+    g_assert (ctx->final_slot_status_timeout_id == 0);
     g_clear_object (&ctx->current_sim);
     g_list_free_full (ctx->sorted_sims, (GDestroyNotify)g_object_unref);
     g_clear_pointer (&ctx->sim_slots, g_ptr_array_unref);
@@ -3220,6 +3254,7 @@
         return FALSE;
 
     ctx = g_task_get_task_data (G_TASK (res));
+
     if (sim_slots)
         *sim_slots = g_steal_pointer (&ctx->sim_slots);
     if (primary_sim_slot)
@@ -3227,6 +3262,93 @@
     return TRUE;
 }
 
+/* Compares arrays of QmiPhysicalSlotStatusSlot */
+static gboolean
+compare_slot_status (GArray *slot_status1,
+                     GArray *slot_status2)
+{
+    guint i;
+    guint j;
+
+    if (!slot_status1 && !slot_status2)
+        return TRUE;
+    if (!slot_status1 || !slot_status2 || slot_status1->len != slot_status2->len)
+        return FALSE;
+    for (i = 0; i < slot_status1->len; i++) {
+        /* Compare slot at index i from slot_status1 and slot_status2 */
+        QmiPhysicalSlotStatusSlot *slot_a;
+        QmiPhysicalSlotStatusSlot *slot_b;
+
+        slot_a = &g_array_index (slot_status1, QmiPhysicalSlotStatusSlot, i);
+        slot_b = &g_array_index (slot_status2, QmiPhysicalSlotStatusSlot, i);
+        if (slot_a->physical_card_status != slot_b->physical_card_status)
+            return FALSE;
+        if (slot_a->physical_slot_status != slot_b->physical_slot_status)
+            return FALSE;
+        if (slot_a->iccid->len != slot_b->iccid->len)
+            return FALSE;
+
+        for (j = 0; j < slot_a->iccid->len; j++) {
+            if (g_array_index (slot_a->iccid, guint8, j) != g_array_index (slot_b->iccid, guint8, j))
+                return FALSE;
+        }
+    }
+    return TRUE;
+}
+
+static gboolean
+hotswap_while_loading_slots (GTask *task)
+{
+    MMIfaceModem *self;
+
+    self = g_task_get_source_object (task);
+
+    mm_obj_dbg (self,
+                "Slot status before loading sim slots is different from slot status after loading sim slots, "
+                "assuming hotswap");
+    clear_load_sim_slot_callbacks (task);
+    g_task_return_new_error (task,
+                             MM_CORE_ERROR,
+                             MM_CORE_ERROR_ABORTED,
+                             "Timed out waiting for final slot status to match initial slot status");
+    g_object_unref (task);
+
+    mm_base_modem_process_sim_event (MM_BASE_MODEM (self));
+    return G_SOURCE_REMOVE;
+}
+
+#define FINAL_SLOT_STATUS_TIMEOUT 3
+
+static void
+check_final_slot_status (GTask *task)
+{
+    LoadSimSlotsContext *ctx;
+    MMIfaceModem        *self;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    /* We may never receive a slot status indication, which means that
+     * no slot switch was performed while loading sim slots */
+    if (!ctx->final_slot_status || compare_slot_status (ctx->initial_slot_status, ctx->final_slot_status)) {
+        mm_obj_dbg (self, "Final slot status matches initial slot status");
+        clear_load_sim_slot_callbacks (task);
+        g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    if (!ctx->final_slot_status_timeout_id) {
+        /* Setup a timeout before which final and initial slot status should match */
+        mm_obj_dbg (self,
+                    "Final slot status does not match initial slot status. "
+                    "Waiting for final slot status indication...");
+        ctx->final_slot_status_timeout_id = g_timeout_add_seconds (FINAL_SLOT_STATUS_TIMEOUT,
+                                                                   (GSourceFunc) hotswap_while_loading_slots,
+                                                                   self);
+    }
+}
+
 static void
 active_slot_switch_ready (QmiClientUim *client,
                           GAsyncResult *res,
@@ -3244,10 +3366,13 @@
     if ((!output || !qmi_message_uim_switch_slot_output_get_result (output, &error)) &&
         !g_error_matches (error, QMI_PROTOCOL_ERROR, QMI_PROTOCOL_ERROR_NO_EFFECT)) {
         mm_obj_err (self, "couldn't switch to original slot %u", ctx->active_slot_number);
+        clear_load_sim_slot_callbacks (task);
         g_task_return_error (task, g_steal_pointer (&error));
-    } else
-        g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
+        g_object_unref (task);
+        return;
+    }
+
+    check_final_slot_status (task);
 }
 
 static void
@@ -3262,8 +3387,10 @@
 
     /* If we're already in the original active SIM slot, nothing else to do */
     if (ctx->current_slot_number == ctx->active_slot_number) {
-        g_task_return_boolean (task, TRUE);
-        g_object_unref (task);
+        mm_obj_dbg (self,
+                    "Already on the original active SIM at slot %u. "
+                    "No more slot switches necessary", ctx->active_slot_number);
+        check_final_slot_status (task);
         return;
     }
 
@@ -3379,6 +3506,35 @@
 }
 
 static void
+load_sim_slots_indication_cb (QmiClientUim                     *client,
+                              QmiIndicationUimSlotStatusOutput *output,
+                              GTask                            *task)
+{
+    g_autoptr(GError)    error = NULL;
+    LoadSimSlotsContext *ctx;
+    MMIfaceModem        *self;
+    GArray              *physical_slots = NULL;
+
+    self = g_task_get_source_object (task);
+    ctx  = g_task_get_task_data (task);
+
+    mm_obj_dbg (self, "received slot status indication while loading slots");
+    if (!qmi_indication_uim_slot_status_output_get_physical_slot_status (output,
+                                                                         &physical_slots,
+                                                                         &error)) {
+        mm_obj_warn (self, "could not process slot status indication: %s", error->message);
+        return;
+    }
+
+    g_clear_object (&ctx->final_slot_status);
+    ctx->final_slot_status = g_array_ref (physical_slots);
+
+    /* We are awaiting the final slot status before we finish the load_sim_slots task */
+    if (ctx->final_slot_status_timeout_id)
+        check_final_slot_status (task);
+}
+
+static void
 uim_get_slot_status_ready (QmiClientUim *client,
                            GAsyncResult *res,
                            GTask        *task)
@@ -3386,6 +3542,7 @@
     g_autoptr(QmiMessageUimGetSlotStatusOutput) output = NULL;
     LoadSimSlotsContext *ctx;
     MMIfaceModem              *self;
+    Private                   *priv;
     GError                    *error = NULL;
     GArray                    *physical_slots = NULL;
     GArray                    *ext_information = NULL;
@@ -3394,6 +3551,7 @@
 
     self = g_task_get_source_object (task);
     ctx  = g_task_get_task_data (task);
+    priv = get_private (MM_SHARED_QMI (self));
 
     output = qmi_client_uim_get_slot_status_finish (client, res, &error);
     if (!output ||
@@ -3404,6 +3562,11 @@
         return;
     }
 
+    /* Store the slot status before loading all sim slots.
+     * We know we are done loading sim slots when the final slot status
+     * indication is the same as initial slot status  */
+    ctx->initial_slot_status = g_array_ref (physical_slots);
+
     /* It's fine if we don't have EID information, but it should be well-formed if present. If it's malformed,
      * there is probably a modem firmware bug. */
     if (qmi_message_uim_get_slot_status_output_get_physical_slot_information (output, &ext_information, NULL) &&
@@ -3491,6 +3654,17 @@
     }
     g_assert_cmpuint (ctx->sim_slots->len, ==, physical_slots->len);
 
+    /* Block hotswap on slot_status_indications since
+     * we will be switching slots while loading them. */
+    if (priv->uim_slot_status_indication_id)
+        g_signal_handler_block (client, priv->uim_slot_status_indication_id);
+
+    /* Monitor slot status indications while we are loading slots. Once we are done loading slots,
+     * check that no hotswap occurred. */
+    ctx->load_sim_slots_indication_id = g_signal_connect (priv->uim_client,
+                                                          "slot-status",
+                                                          G_CALLBACK (load_sim_slots_indication_cb),
+                                                          task);
     /* Now, iterate over all the SIMs, we'll attempt to load info from them by
      * quickly switching over to them, leaving the active SIM to the end */
     load_next_sim_info (task);
@@ -3780,7 +3954,6 @@
                                MMSharedQmi                      *self)
 {
     GArray            *physical_slots = NULL;
-    guint              i;
     g_autoptr(GError)  error = NULL;
 
     mm_obj_dbg (self, "received slot status indication");
@@ -3792,27 +3965,13 @@
         return;
     }
 
-    for (i = 0; i < physical_slots->len; i++) {
-        QmiPhysicalSlotStatusSlot *slot_status;
-
-        slot_status = &g_array_index (physical_slots, QmiPhysicalSlotStatusSlot, i);
-
-        /* We only care about active slot changes */
-        if (slot_status->physical_slot_status == QMI_UIM_SLOT_STATE_ACTIVE) {
-            g_autofree gchar *iccid = NULL;
-
-            if (slot_status->iccid && slot_status->iccid->len > 0) {
-                iccid = mm_bcd_to_string ((const guint8 *) slot_status->iccid->data, slot_status->iccid->len,
-                                          TRUE /* low_nybble_first */);
-            }
-
-            mm_iface_modem_check_for_sim_swap (MM_IFACE_MODEM (self),
-                                               i + 1, /* Slot index */
-                                               iccid,
-                                               NULL,
-                                               NULL);
-        }
-    }
+    /* A slot status indication means that
+     * 1) The physical slot to logical slot mapping has changed as a
+     * result of switching the slot. or,
+     * 2) A card has been removed from, or inserted to, the physical slot. or,
+     * 3) A physical slot is powered up or down. or,
+     * In all these cases, we must reprobe the modem to keep SIM objects updated */
+    mm_base_modem_process_sim_event (MM_BASE_MODEM (self));
 }
 
 static void
diff --git a/src/mm-sms-part-3gpp.c b/src/mm-sms-part-3gpp.c
index c18aaa7..fbf8e11 100644
--- a/src/mm-sms-part-3gpp.c
+++ b/src/mm-sms-part-3gpp.c
@@ -120,23 +120,26 @@
 }
 
 /* len is in semi-octets */
-static char *
-sms_decode_address (const guint8 *address, int len)
+static gchar *
+sms_decode_address (const guint8  *address,
+                    gint           len,
+                    GError       **error)
 {
     guint8 addrtype, addrplan;
-    char *utf8;
+    gchar *utf8;
 
     addrtype = address[0] & SMS_NUMBER_TYPE_MASK;
     addrplan = address[0] & SMS_NUMBER_PLAN_MASK;
     address++;
 
     if (addrtype == SMS_NUMBER_TYPE_ALPHA) {
-        guint8 *unpacked;
-        guint32 unpacked_len;
+        g_autoptr(GByteArray)  unpacked_array = NULL;
+        guint8                *unpacked = NULL;
+        guint32                unpacked_len;
+
         unpacked = mm_charset_gsm_unpack (address, (len * 4) / 7, 0, &unpacked_len);
-        utf8 = (char *)mm_charset_gsm_unpacked_to_utf8 (unpacked,
-                                                        unpacked_len);
-        g_free (unpacked);
+        unpacked_array = g_byte_array_new_take (unpacked, unpacked_len);
+        utf8 = mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error);
     } else if (addrtype == SMS_NUMBER_TYPE_INTL &&
                addrplan == SMS_NUMBER_PLAN_TELEPHONE) {
         /* International telphone number, format as "+1234567890" */
@@ -239,41 +242,44 @@
     return scheme;
 }
 
-static char *
-sms_decode_text (const guint8 *text,
-                 int           len,
-                 MMSmsEncoding encoding,
-                 int           bit_offset,
-                 gpointer      log_object)
+static gchar *
+sms_decode_text (const guint8   *text,
+                 int             len,
+                 MMSmsEncoding   encoding,
+                 int             bit_offset,
+                 gpointer        log_object,
+                 GError        **error)
 {
-    gchar *utf8;
-
     if (encoding == MM_SMS_ENCODING_GSM7) {
-        g_autofree guint8 *unpacked = NULL;
-        guint32            unpacked_len;
+        g_autoptr(GByteArray)  unpacked_array = NULL;
+        guint8                *unpacked = NULL;
+        guint32                unpacked_len;
+        gchar                 *utf8;
 
-        mm_obj_dbg (log_object, "converting SMS part text from GSM-7 to UTF-8...");
         unpacked = mm_charset_gsm_unpack ((const guint8 *) text, len, bit_offset, &unpacked_len);
-        utf8 = (char *) mm_charset_gsm_unpacked_to_utf8 (unpacked, unpacked_len);
-        mm_obj_dbg (log_object, "   got UTF-8 text: '%s'", utf8);
-    } else if (encoding == MM_SMS_ENCODING_UCS2) {
-        g_autoptr(GByteArray) bytearray = NULL;
-
-        mm_obj_dbg (log_object, "converting SMS part text from UTF-16BE to UTF-8...");
-        bytearray = g_byte_array_append (g_byte_array_sized_new (len), (const guint8 *)text, len);
-        /* Always assume UTF-16 instead of UCS-2! */
-        utf8 = mm_modem_charset_byte_array_to_utf8 (bytearray, MM_MODEM_CHARSET_UTF16);
-        if (!utf8) {
-            mm_obj_warn (log_object, "couldn't convert SMS part contents from UTF-16BE to UTF-8: not decoding any text");
-            utf8 = g_strdup ("");
-        } else
-            mm_obj_dbg (log_object, "   got UTF-8 text: '%s'", utf8);
-    } else {
-        mm_obj_warn (log_object, "unexpected encoding: %s; not decoding any text", mm_sms_encoding_get_string (encoding));
-        utf8 = g_strdup ("");
+        unpacked_array = g_byte_array_new_take (unpacked, unpacked_len);
+        utf8 = mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error);
+        if (utf8)
+            mm_obj_dbg (log_object, "converted SMS part text from GSM-7 to UTF-8: %s", utf8);
+        return utf8;
     }
 
-    return utf8;
+    /* Always assume UTF-16 instead of UCS-2! */
+    if (encoding == MM_SMS_ENCODING_UCS2) {
+        g_autoptr(GByteArray)  bytearray = NULL;
+        gchar                 *utf8;
+
+        bytearray = g_byte_array_append (g_byte_array_sized_new (len), (const guint8 *)text, len);
+        utf8 = mm_modem_charset_bytearray_to_utf8 (bytearray, MM_MODEM_CHARSET_UTF16, FALSE, error);
+        if (utf8)
+            mm_obj_dbg (log_object, "converted SMS part text from UTF-16BE to UTF-8: %s", utf8);
+        return utf8;
+    }
+
+    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                 "Couldn't convert SMS part contents from %s to UTF-8",
+                 mm_sms_encoding_get_string (encoding));
+    return NULL;
 }
 
 static guint
@@ -339,24 +345,17 @@
                                gpointer      log_object,
                                GError      **error)
 {
-    gsize pdu_len;
-    guint8 *pdu;
-    MMSmsPart *part;
+    g_autofree guint8 *pdu = NULL;
+    gsize              pdu_len;
 
     /* Convert PDU from hex to binary */
-    pdu = (guint8 *) mm_utils_hexstr2bin (hexpdu, &pdu_len);
+    pdu = mm_utils_hexstr2bin (hexpdu, -1, &pdu_len, error);
     if (!pdu) {
-        g_set_error_literal (error,
-                             MM_CORE_ERROR,
-                             MM_CORE_ERROR_FAILED,
-                             "Couldn't convert 3GPP PDU from hex to binary");
+        g_prefix_error (error, "Couldn't convert 3GPP PDU from hex to binary: ");
         return NULL;
     }
 
-    part = mm_sms_part_3gpp_new_from_binary_pdu (index, pdu, pdu_len, log_object, error);
-    g_free (pdu);
-
-    return part;
+    return mm_sms_part_3gpp_new_from_binary_pdu (index, pdu, pdu_len, log_object, error);
 }
 
 MMSmsPart *
@@ -380,6 +379,7 @@
     guint tp_dcs_offset = 0;
     guint tp_user_data_len_offset = 0;
     MMSmsEncoding user_data_encoding = MM_SMS_ENCODING_UNKNOWN;
+    gchar *address;
 
     /* Create the new MMSmsPart */
     sms_part = mm_sms_part_new (index, MM_SMS_PDU_TYPE_UNKNOWN);
@@ -412,8 +412,13 @@
     if (smsc_addr_size_bytes > 0) {
         PDU_SIZE_CHECK (offset + smsc_addr_size_bytes, "cannot read SMSC address");
         /* SMSC may not be given in DELIVER PDUs */
-        mm_sms_part_take_smsc (sms_part,
-                               sms_decode_address (&pdu[1], 2 * (smsc_addr_size_bytes - 1)));
+        address = sms_decode_address (&pdu[1], 2 * (smsc_addr_size_bytes - 1), error);
+        if (!address) {
+            g_prefix_error (error, "Couldn't read SMSC address: ");
+            mm_sms_part_free (sms_part);
+            return NULL;
+        }
+        mm_sms_part_take_smsc (sms_part, g_steal_pointer (&address));
         mm_obj_dbg (log_object, "  SMSC address parsed: '%s'", mm_sms_part_get_smsc (sms_part));
         offset += smsc_addr_size_bytes;
     } else
@@ -485,9 +490,13 @@
     tp_addr_size_bytes = (tp_addr_size_digits + 1) >> 1;
 
     PDU_SIZE_CHECK (offset + tp_addr_size_bytes, "cannot read number");
-    mm_sms_part_take_number (sms_part,
-                             sms_decode_address (&pdu[offset],
-                                                 tp_addr_size_digits));
+    address = sms_decode_address (&pdu[offset], tp_addr_size_digits, error);
+    if (!address) {
+        g_prefix_error (error, "Couldn't read address: ");
+        mm_sms_part_free (sms_part);
+        return NULL;
+    }
+    mm_sms_part_take_number (sms_part, g_steal_pointer (&address));
     mm_obj_dbg (log_object, "  number parsed: %s", mm_sms_part_get_number (sms_part));
     offset += (1 + tp_addr_size_bytes); /* +1 due to the Type of Address byte */
 
@@ -716,17 +725,24 @@
         switch (user_data_encoding) {
         case MM_SMS_ENCODING_GSM7:
         case MM_SMS_ENCODING_UCS2:
-            /* Otherwise if it's 7-bit or UCS2 we can decode it */
-            mm_obj_dbg (log_object, "decoding SMS text with %u elements", tp_user_data_size_elements);
-            mm_sms_part_take_text (sms_part,
-                                   sms_decode_text (&pdu[tp_user_data_offset],
-                                                    tp_user_data_size_elements,
-                                                    user_data_encoding,
-                                                    bit_offset,
-                                                    log_object));
-            g_warn_if_fail (mm_sms_part_get_text (sms_part) != NULL);
-            break;
+            {
+                gchar *text;
 
+                /* Otherwise if it's 7-bit or UCS2 we can decode it */
+                mm_obj_dbg (log_object, "decoding SMS text with %u elements", tp_user_data_size_elements);
+                text = sms_decode_text (&pdu[tp_user_data_offset],
+                                        tp_user_data_size_elements,
+                                        user_data_encoding,
+                                        bit_offset,
+                                        log_object,
+                                        error);
+                if (!text) {
+                    mm_sms_part_free (sms_part);
+                    return NULL;
+                }
+                mm_sms_part_take_text (sms_part, text);
+                break;
+            }
         case MM_SMS_ENCODING_8BIT:
         case MM_SMS_ENCODING_UNKNOWN:
         default:
@@ -969,12 +985,15 @@
     }
 
     if (encoding == MM_SMS_ENCODING_GSM7) {
-        guint8 *unpacked, *packed;
-        guint32 unlen = 0, packlen = 0;
+        g_autoptr(GByteArray)  unpacked = NULL;
+        g_autofree guint8     *packed = NULL;
+        guint32                packlen = 0;
 
-        unpacked = mm_charset_utf8_to_unpacked_gsm (mm_sms_part_get_text (part), &unlen);
-        if (!unpacked || unlen == 0) {
-            g_free (unpacked);
+        unpacked = mm_modem_charset_bytearray_from_utf8 (mm_sms_part_get_text (part), MM_MODEM_CHARSET_GSM, FALSE, error);
+        if (!unpacked)
+            goto error;
+
+        if (unpacked->len == 0) {
             g_set_error_literal (error,
                                  MM_MESSAGE_ERROR,
                                  MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER,
@@ -985,15 +1004,13 @@
         /* Set real data length, in septets
          * If we had UDH, add 7 septets
          */
-        *udl_ptr = mm_sms_part_get_concat_sequence (part) ? (7 + unlen) : unlen;
+        *udl_ptr = mm_sms_part_get_concat_sequence (part) ? (7 + unpacked->len) : unpacked->len;
         mm_obj_dbg (log_object, "  user data length is %u septets (%s UDH)",
                     *udl_ptr,
                     mm_sms_part_get_concat_sequence (part) ? "with" : "without");
 
-        packed = mm_charset_gsm_pack (unpacked, unlen, shift, &packlen);
-        g_free (unpacked);
+        packed = mm_charset_gsm_pack (unpacked->data, unpacked->len, shift, &packlen);
         if (!packed || packlen == 0) {
-            g_free (packed);
             g_set_error_literal (error,
                                  MM_MESSAGE_ERROR,
                                  MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER,
@@ -1002,16 +1019,14 @@
         }
 
         memcpy (&pdu[offset], packed, packlen);
-        g_free (packed);
         offset += packlen;
     } else if (encoding == MM_SMS_ENCODING_UCS2) {
         g_autoptr(GByteArray) array = NULL;
         g_autoptr(GError)     inner_error = NULL;
 
-        /* Try to guess a good value for the array */
-        array = g_byte_array_sized_new (strlen (mm_sms_part_get_text (part)) * 2);
         /* Always assume UTF-16 instead of UCS-2! */
-        if (!mm_modem_charset_byte_array_append (array, mm_sms_part_get_text (part), FALSE, MM_MODEM_CHARSET_UTF16, &inner_error)) {
+        array = mm_modem_charset_bytearray_from_utf8 (mm_sms_part_get_text (part), MM_MODEM_CHARSET_UTF16, FALSE, &inner_error);
+        if (!array) {
             g_set_error (error,
                          MM_MESSAGE_ERROR,
                          MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER,
diff --git a/src/mm-sms-part-cdma.c b/src/mm-sms-part-cdma.c
index e08193c..d79a0f9 100644
--- a/src/mm-sms-part-cdma.c
+++ b/src/mm-sms-part-cdma.c
@@ -317,24 +317,17 @@
                                gpointer      log_object,
                                GError      **error)
 {
-    gsize pdu_len;
-    guint8 *pdu;
-    MMSmsPart *part;
+    g_autofree guint8 *pdu = NULL;
+    gsize              pdu_len;
 
     /* Convert PDU from hex to binary */
-    pdu = (guint8 *) mm_utils_hexstr2bin (hexpdu, &pdu_len);
+    pdu = mm_utils_hexstr2bin (hexpdu, -1, &pdu_len, error);
     if (!pdu) {
-        g_set_error_literal (error,
-                             MM_CORE_ERROR,
-                             MM_CORE_ERROR_FAILED,
-                             "Couldn't convert CDMA PDU from hex to binary");
+        g_prefix_error (error, "Couldn't convert CDMA PDU from hex to binary: ");
         return NULL;
     }
 
-    part = mm_sms_part_cdma_new_from_binary_pdu (index, pdu, pdu_len, log_object, error);
-    g_free (pdu);
-
-    return part;
+    return mm_sms_part_cdma_new_from_binary_pdu (index, pdu, pdu_len, log_object, error);
 }
 
 struct Parameter {
@@ -1375,65 +1368,49 @@
     return TRUE;
 }
 
-static void
+static GByteArray *
 decide_best_encoding (const gchar *text,
                       gpointer     log_object,
-                      GByteArray **out,
                       guint       *num_fields,
                       guint       *num_bits_per_field,
-                      Encoding    *encoding)
+                      Encoding    *encoding,
+                      GError     **error)
 {
-    guint ascii_unsupported = 0;
-    guint i;
-    guint len;
-    g_autoptr(GError) error = NULL;
+    g_autoptr(GByteArray) barray = NULL;
+    MMModemCharset        target_charset = MM_MODEM_CHARSET_UNKNOWN;
+    guint                 len;
 
     len = strlen (text);
 
-    /* Check if we can do ASCII-7 */
-    for (i = 0; i < len; i++) {
-        if (text[i] & 0x80) {
-            ascii_unsupported++;
-            break;
-        }
+    if (mm_charset_can_convert_to (text, MM_MODEM_CHARSET_IRA))
+        target_charset = MM_MODEM_CHARSET_IRA;
+    else if (mm_charset_can_convert_to (text, MM_MODEM_CHARSET_8859_1))
+        target_charset = MM_MODEM_CHARSET_8859_1;
+    else
+        target_charset = MM_MODEM_CHARSET_UCS2;
+
+    barray = mm_modem_charset_bytearray_from_utf8 (text, target_charset, FALSE, error);
+    if (!barray) {
+        g_prefix_error (error, "Couldn't decide best encoding: ");
+        return NULL;
     }
 
-    /* If ASCII-7 already supported, done we are */
-    if (!ascii_unsupported) {
-        *out = g_byte_array_sized_new (len);
-        g_byte_array_append (*out, (const guint8 *)text, len);
+    if (target_charset == MM_MODEM_CHARSET_IRA) {
         *num_fields = len;
         *num_bits_per_field = 7;
         *encoding = ENCODING_ASCII_7BIT;
-        return;
-    }
-
-    /* Check if we can do Latin encoding */
-    if (mm_charset_can_convert_to (text, MM_MODEM_CHARSET_8859_1)) {
-        *out = g_byte_array_sized_new (len);
-        if (!mm_modem_charset_byte_array_append (*out,
-                                                 text,
-                                                 FALSE,
-                                                 MM_MODEM_CHARSET_8859_1,
-                                                 &error))
-            mm_obj_warn (log_object, "failed to convert to latin encoding: %s", error->message);
-        *num_fields = (*out)->len;
+    } else if (target_charset == MM_MODEM_CHARSET_8859_1) {
+        *num_fields = barray->len;
         *num_bits_per_field = 8;
         *encoding = ENCODING_LATIN;
-        return;
-    }
+    } else if (target_charset == MM_MODEM_CHARSET_UCS2) {
+        *num_fields = barray->len / 2;
+        *num_bits_per_field = 16;
+        *encoding = ENCODING_UNICODE;
+    } else
+        g_assert_not_reached ();
 
-    /* If no Latin and no ASCII, default to UTF-16 */
-    *out = g_byte_array_sized_new (len * 2);
-    if (!mm_modem_charset_byte_array_append (*out,
-                                             text,
-                                             FALSE,
-                                             MM_MODEM_CHARSET_UCS2,
-                                             &error))
-        mm_obj_warn (log_object, "failed to convert to UTF-16 encoding: %s", error->message);
-    *num_fields = (*out)->len / 2;
-    *num_bits_per_field = 16;
-    *encoding = ENCODING_UNICODE;
+    return g_steal_pointer (&barray);
 }
 
 static gboolean
@@ -1477,12 +1454,14 @@
 
     /* Text or Data */
     if (text) {
-        decide_best_encoding (text,
-                              log_object,
-                              &converted,
-                              &num_fields,
-                              &num_bits_per_field,
-                              &encoding);
+        converted = decide_best_encoding (text,
+                                          log_object,
+                                          &num_fields,
+                                          &num_bits_per_field,
+                                          &encoding,
+                                          error);
+        if (!converted)
+            return FALSE;
         aux = (const GByteArray *)converted;
     } else {
         aux = data;
diff --git a/src/mm-utils.c b/src/mm-utils.c
new file mode 100644
index 0000000..95fbb55
--- /dev/null
+++ b/src/mm-utils.c
@@ -0,0 +1,47 @@
+/* -*- 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:
+ *
+ * Singleton support imported from NetworkManager.
+ *     (C) Copyright 2014 Red Hat, Inc.
+ *
+ * GPtrArray lookup with GEqualFunc imported from GLib 2.48
+ */
+
+#include "mm-utils.h"
+
+#if !GLIB_CHECK_VERSION(2,54,0)
+
+gboolean
+mm_ptr_array_find_with_equal_func (GPtrArray     *haystack,
+				   gconstpointer  needle,
+				   GEqualFunc     equal_func,
+				   guint         *index_)
+{
+  guint i;
+
+  g_return_val_if_fail (haystack != NULL, FALSE);
+
+  if (equal_func == NULL)
+      equal_func = g_direct_equal;
+
+  for (i = 0; i < haystack->len; i++) {
+      if (equal_func (g_ptr_array_index (haystack, i), needle)) {
+          if (index_ != NULL)
+              *index_ = i;
+          return TRUE;
+      }
+  }
+
+  return FALSE;
+}
+
+#endif
diff --git a/src/mm-utils.h b/src/mm-utils.h
index cdb123c..613205a 100644
--- a/src/mm-utils.h
+++ b/src/mm-utils.h
@@ -12,6 +12,8 @@
  *
  * Singleton support imported from NetworkManager.
  *     (C) Copyright 2014 Red Hat, Inc.
+ *
+ * GPtrArray lookup with GEqualFunc imported from GLib 2.48
  */
 
 #ifndef MM_UTILS_H
@@ -78,4 +80,16 @@
     }                                                                   \
     MM_DEFINE_SINGLETON_DESTRUCTOR(TYPE)
 
+
+#if !GLIB_CHECK_VERSION(2,54,0)
+
+/* Pointer Array lookup with a GEqualFunc, imported from GLib 2.54 */
+#define g_ptr_array_find_with_equal_func mm_ptr_array_find_with_equal_func
+gboolean mm_ptr_array_find_with_equal_func (GPtrArray     *haystack,
+                                            gconstpointer  needle,
+                                            GEqualFunc     equal_func,
+                                            guint         *index_);
+
+#endif
+
 #endif /* MM_UTILS_H */
diff --git a/src/tests/test-charsets.c b/src/tests/test-charsets.c
index a15e033..8735fd2 100644
--- a/src/tests/test-charsets.c
+++ b/src/tests/test-charsets.c
@@ -23,23 +23,24 @@
 static void
 common_test_gsm7 (const gchar *in_utf8)
 {
-    guint32 unpacked_gsm_len = 0;
     guint32 packed_gsm_len = 0;
     guint32 unpacked_gsm_len_2 = 0;
-    g_autofree guint8 *unpacked_gsm = NULL;
+    g_autoptr(GByteArray) unpacked_gsm = NULL;
     g_autofree guint8 *packed_gsm = NULL;
-    g_autofree guint8 *unpacked_gsm_2 = NULL;
+    guint8 *unpacked_gsm_2 = NULL;
+    g_autoptr(GByteArray) unpacked_gsm_2_array = NULL;
     g_autofree gchar *built_utf8 = NULL;
+    g_autoptr(GError) error = NULL;
 
     /* Convert to GSM */
-    unpacked_gsm = mm_charset_utf8_to_unpacked_gsm (in_utf8, &unpacked_gsm_len);
+    unpacked_gsm = mm_modem_charset_bytearray_from_utf8 (in_utf8, MM_MODEM_CHARSET_GSM, FALSE, &error);
     g_assert_nonnull (unpacked_gsm);
-    g_assert_cmpuint (unpacked_gsm_len, >, 0);
+    g_assert_no_error (error);
 
     /* Pack */
-    packed_gsm = mm_charset_gsm_pack (unpacked_gsm, unpacked_gsm_len, 0, &packed_gsm_len);
+    packed_gsm = mm_charset_gsm_pack (unpacked_gsm->data, unpacked_gsm->len, 0, &packed_gsm_len);
     g_assert_nonnull (packed_gsm);
-    g_assert_cmpuint (packed_gsm_len, <=, unpacked_gsm_len);
+    g_assert_cmpuint (packed_gsm_len, <=, unpacked_gsm->len);
 
 #if 0
     {
@@ -56,10 +57,12 @@
     /* Unpack */
     unpacked_gsm_2 = mm_charset_gsm_unpack (packed_gsm, packed_gsm_len * 8 / 7, 0, &unpacked_gsm_len_2);
     g_assert_nonnull (unpacked_gsm_2);
+    unpacked_gsm_2_array = g_byte_array_new_take (unpacked_gsm_2, unpacked_gsm_len_2);
 
     /* And back to UTF-8 */
-    built_utf8 = (gchar *) mm_charset_gsm_unpacked_to_utf8 (unpacked_gsm_2, unpacked_gsm_len_2);
+    built_utf8 = mm_modem_charset_bytearray_to_utf8 (unpacked_gsm_2_array, MM_MODEM_CHARSET_GSM, FALSE, &error);
     g_assert_nonnull (built_utf8);
+    g_assert_no_error (error);
     g_assert_cmpstr (built_utf8, ==, in_utf8);
 }
 
@@ -314,53 +317,71 @@
 }
 
 static void
-test_take_convert_ucs2_hex_utf8 (void)
+test_str_ucs2_to_from_utf8 (void)
 {
-    gchar *src, *converted, *utf8;
+    const gchar       *src = "0054002D004D006F00620069006C0065";
+    g_autofree gchar  *utf8 = NULL;
+    g_autofree gchar  *dst = NULL;
+    g_autoptr(GError)  error = NULL;
 
-    /* Ensure hex-encoded UCS-2 works */
-    src = g_strdup ("0054002d004d006f00620069006c0065");
-    converted = mm_charset_take_and_convert_to_utf8 (src, MM_MODEM_CHARSET_UCS2);
-    g_assert_cmpstr (converted, ==, "T-Mobile");
-    utf8 = mm_utf8_take_and_convert_to_charset (converted, MM_MODEM_CHARSET_UCS2);
-    g_assert_cmpstr (utf8, ==, "0054002D004D006F00620069006C0065");
-    g_free (utf8);
-}
-
-static void
-test_take_convert_ucs2_bad_ascii (void)
-{
-    gchar *src, *converted;
-
-    /* Test that something mostly ASCII returns most of the original string */
-    src = g_strdup ("Orange\241");
-    converted = mm_charset_take_and_convert_to_utf8 (src, MM_MODEM_CHARSET_UCS2);
-    g_assert_cmpstr (converted, ==, "Orange");
-    g_free (converted);
-}
-
-static void
-test_take_convert_ucs2_bad_ascii2 (void)
-{
-    gchar *src, *converted;
-
-    /* Ensure something completely screwed up doesn't crash */
-    src = g_strdup ("\241\255\254\250\244\234");
-    converted = mm_charset_take_and_convert_to_utf8 (src, MM_MODEM_CHARSET_UCS2);
-    g_assert (converted == NULL);
-}
-
-static void
-test_take_convert_gsm_utf8 (void)
-{
-    gchar *src, *converted, *utf8;
-
-    src = g_strdup ("T-Mobile");
-    converted = mm_charset_take_and_convert_to_utf8 (src, MM_MODEM_CHARSET_GSM);
-    g_assert_cmpstr (converted, ==, "T-Mobile");
-    utf8 = mm_utf8_take_and_convert_to_charset (converted, MM_MODEM_CHARSET_GSM);
+    utf8 = mm_modem_charset_str_to_utf8 (src, -1, MM_MODEM_CHARSET_UCS2, FALSE, &error);
+    g_assert_no_error (error);
     g_assert_cmpstr (utf8, ==, "T-Mobile");
-    g_free (utf8);
+
+    dst = mm_modem_charset_str_from_utf8 (utf8, MM_MODEM_CHARSET_UCS2, FALSE, &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (dst, ==, src);
+}
+
+static void
+test_str_gsm_to_from_utf8 (void)
+{
+    const gchar       *src = "T-Mobile";
+    g_autofree gchar  *utf8 = NULL;
+    g_autofree gchar  *dst = NULL;
+    g_autoptr(GError)  error = NULL;
+
+    /* Note: as long as the GSM string doesn't contain the '@' character, str_to_utf8()
+     * and str_from_utf8() can safely be used */
+
+    utf8 = mm_modem_charset_str_to_utf8 (src, -1, MM_MODEM_CHARSET_GSM, FALSE, &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (utf8, ==, src);
+
+    dst = mm_modem_charset_str_from_utf8 (utf8, MM_MODEM_CHARSET_GSM, FALSE, &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (dst, ==, src);
+}
+
+static void
+test_str_gsm_to_from_utf8_with_at (void)
+{
+    /* The NULs are '@' chars, except for the trailing one which is always taken as end-of-string */
+    const gchar        src[] = { 'T', '-', 'M', 0x00, 'o', 'b', 'i', 0x00, 'l', 'e', 0x00 };
+    const gchar       *utf8_expected = "T-M@obi@le";
+    const gchar       *src_translit = "T-M?obi?le";
+    g_autofree gchar  *utf8 = NULL;
+    g_autofree gchar  *dst = NULL;
+    g_autoptr(GError)  error = NULL;
+
+    /* Note: as long as the GSM string doesn't contain the '@' character, str_to_utf8()
+     * and str_from_utf8() can safely be used */
+
+    utf8 = mm_modem_charset_str_to_utf8 (src, G_N_ELEMENTS (src), MM_MODEM_CHARSET_GSM, FALSE, &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (utf8, ==, utf8_expected);
+
+    /* if charset conversion from UTF-8 contains '@' chars, running without transliteration
+     * will return an error */
+    dst = mm_modem_charset_str_from_utf8 (utf8, MM_MODEM_CHARSET_GSM, FALSE, &error);
+    g_assert_nonnull (error);
+    g_assert_null (dst);
+    g_clear_error (&error);
+
+    /* with transliteration, '@'->'?' */
+    dst = mm_modem_charset_str_from_utf8 (utf8, MM_MODEM_CHARSET_GSM, TRUE, &error);
+    g_assert_no_error (error);
+    g_assert_cmpstr (dst, ==, src_translit);
 }
 
 struct charset_can_convert_to_test_s {
@@ -444,10 +465,9 @@
     g_test_add_func ("/MM/charsets/gsm7/pack/last-septet-alone", test_gsm7_pack_last_septet_alone);
     g_test_add_func ("/MM/charsets/gsm7/pack/7-chars-offset",    test_gsm7_pack_7_chars_offset);
 
-    g_test_add_func ("/MM/charsets/take-convert/ucs2/hex",         test_take_convert_ucs2_hex_utf8);
-    g_test_add_func ("/MM/charsets/take-convert/ucs2/bad-ascii",   test_take_convert_ucs2_bad_ascii);
-    g_test_add_func ("/MM/charsets/take-convert/ucs2/bad-ascii-2", test_take_convert_ucs2_bad_ascii2);
-    g_test_add_func ("/MM/charsets/take-convert/gsm",              test_take_convert_gsm_utf8);
+    g_test_add_func ("/MM/charsets/str-from-to/ucs2",         test_str_ucs2_to_from_utf8);
+    g_test_add_func ("/MM/charsets/str-from-to/gsm",          test_str_gsm_to_from_utf8);
+    g_test_add_func ("/MM/charsets/str-from-to/gsm-with-at",  test_str_gsm_to_from_utf8_with_at);
 
     g_test_add_func ("/MM/charsets/can-convert-to", test_charset_can_covert_to);
 
diff --git a/src/tests/test-modem-helpers.c b/src/tests/test-modem-helpers.c
index a6bc72d..9624c52 100644
--- a/src/tests/test-modem-helpers.c
+++ b/src/tests/test-modem-helpers.c
@@ -626,9 +626,10 @@
 static void
 test_cops_response_mf627a (void *f, gpointer d)
 {
+    /* The '@' in this string is ASCII 0x40, and 0x40 is a valid GSM-7 char: '¡' (which is 0xc2,0xa1 in UTF-8) */
     const char *reply = "+COPS: (2,\"AT&T@\",\"AT&TD\",\"310410\",0),(3,\"Vstream Wireless\",\"VSTREAM\",\"31026\",0),";
     static MM3gppNetworkInfo expected[] = {
-        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT,   (gchar *) "AT&T@",            (gchar *) "AT&TD",   (gchar *) "310410", MM_MODEM_ACCESS_TECHNOLOGY_GSM },
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT,   (gchar *) "AT&T¡",            (gchar *) "AT&TD",   (gchar *) "310410", MM_MODEM_ACCESS_TECHNOLOGY_GSM },
         { MM_MODEM_3GPP_NETWORK_AVAILABILITY_FORBIDDEN, (gchar *) "Vstream Wireless", (gchar *) "VSTREAM", (gchar *) "31026",  MM_MODEM_ACCESS_TECHNOLOGY_GSM },
     };
 
@@ -638,9 +639,10 @@
 static void
 test_cops_response_mf627b (void *f, gpointer d)
 {
+    /* The '@' in this string is ASCII 0x40, and 0x40 is a valid GSM-7 char: '¡' (which is 0xc2,0xa1 in UTF-8) */
     const char *reply = "+COPS: (2,\"AT&Tp\",\"AT&T@\",\"310410\",0),(3,\"\",\"\",\"31026\",0),";
     static MM3gppNetworkInfo expected[] = {
-        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT,   (gchar *) "AT&Tp", (gchar *) "AT&T@", (gchar *) "310410", MM_MODEM_ACCESS_TECHNOLOGY_GSM },
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT,   (gchar *) "AT&Tp", (gchar *) "AT&T¡", (gchar *) "310410", MM_MODEM_ACCESS_TECHNOLOGY_GSM },
         { MM_MODEM_3GPP_NETWORK_AVAILABILITY_FORBIDDEN, NULL,              NULL,              (gchar *) "31026",  MM_MODEM_ACCESS_TECHNOLOGY_GSM },
     };
 
@@ -1041,7 +1043,7 @@
     gchar *str;
 
     str = g_strdup (t->input);
-    mm_3gpp_normalize_operator (&str, t->charset);
+    mm_3gpp_normalize_operator (&str, t->charset, NULL);
     if (!t->normalized)
         g_assert (!str);
     else