Merge "Moved multithread_func related functions to utils.py"
diff --git a/acts/framework/acts/controllers/access_point.py b/acts/framework/acts/controllers/access_point.py
index 0353b59..21e26bb 100755
--- a/acts/framework/acts/controllers/access_point.py
+++ b/acts/framework/acts/controllers/access_point.py
@@ -128,6 +128,13 @@
         additional_ap_parameters: Additional parameters to send the AP.
         password: Password to connect to WLAN if necessary.
         check_connectivity: Whether to check for internet connectivity.
+
+    Returns:
+        An identifier for each ssid being started. These identifiers can be
+        used later by this controller to control the ap.
+
+    Raises:
+        Error: When the ap can't be brought up.
     """
     ap = hostapd_ap_preset.create_ap_preset(profile_name=profile_name,
                                             iface_wlan_2g=access_point.wlan_2g,
@@ -148,9 +155,10 @@
                                             n_capabilities=n_capabilities,
                                             ac_capabilities=ac_capabilities,
                                             vht_bandwidth=vht_bandwidth)
-    access_point.start_ap(hostapd_config=ap,
-                          setup_bridge=setup_bridge,
-                          additional_parameters=additional_ap_parameters)
+    return access_point.start_ap(
+        hostapd_config=ap,
+        setup_bridge=setup_bridge,
+        additional_parameters=additional_ap_parameters)
 
 
 class Error(Exception):
diff --git a/acts/framework/acts/controllers/cellular_lib/LteCaSimulation.py b/acts/framework/acts/controllers/cellular_lib/LteCaSimulation.py
index 6d67b12..1784315 100644
--- a/acts/framework/acts/controllers/cellular_lib/LteCaSimulation.py
+++ b/acts/framework/acts/controllers/cellular_lib/LteCaSimulation.py
@@ -20,9 +20,6 @@
 class LteCaSimulation(LteSimulation.LteSimulation):
     """ Carrier aggregation LTE simulation. """
 
-    # Configuration dictionary keys
-    PARAM_CA = 'ca'
-
     # Test config keywords
     KEY_FREQ_BANDS = "freq_bands"
 
@@ -63,285 +60,72 @@
         self.freq_bands = test_config.get(self.KEY_FREQ_BANDS, True)
 
     def configure(self, parameters):
-        """ Configures simulation using a dictionary of parameters.
-
-        Processes LTE CA configuration parameters.
+        """ Configures PCC and SCCs using a dictionary of parameters.
 
         Args:
-            parameters: a configuration dictionary
+            parameters: a list of configuration dictionaris
         """
-        # Get the CA band configuration
-        if self.PARAM_CA not in parameters:
-            raise ValueError(
-                "The config dictionary must include key '{}' with the CA "
-                "config. For example: ca_3c7c28a".format(self.PARAM_CA))
-
-        # Carrier aggregation configurations are indicated with the band numbers
-        # followed by the CA classes in a single string. For example, for 5 CA
-        # using 3C 7C and 28A the parameter value should be 3c7c28a.
-        ca_configs = re.findall(r'(\d+[abcABC])', parameters[self.PARAM_CA])
-
-        if not ca_configs:
-            raise ValueError(
-                "The CA configuration has to be indicated with one string as "
-                "in the following example: 3c7c28a".format(self.PARAM_CA))
-
-        # Initialize the secondary cells
-        bands = []
-        for ca in ca_configs:
-            ca_class = ca[-1]
-            band = ca[:-1]
-            bands.append(band)
-            if ca_class.upper() == 'B' or ca_class.upper() == 'C':
-                # Class B and C means two carriers with the same band
-                bands.append(band)
-        self.simulator.set_band_combination(bands)
-
-        # Count the number of carriers in the CA combination
-        self.num_carriers = 0
-        for ca in ca_configs:
-            ca_class = ca[-1]
-            # Class C means that there are two contiguous carriers, while other
-            # classes are a single one.
-            if ca_class.upper() == 'C':
-                self.num_carriers += 2
-            else:
-                self.num_carriers += 1
-
-        # Create an array of configuration objects to set up the base stations.
-        new_configs = [self.BtsConfig() for _ in range(self.num_carriers)]
-
-        # Save the bands to the bts config objects
-        bts_index = 0
-        for ca in ca_configs:
-            ca_class = ca[-1]
-            band = ca[:-1]
-
-            new_configs[bts_index].band = band
-            bts_index += 1
-
-            if ca_class.upper() == 'B' or ca_class.upper() == 'C':
-                # Class B and C means two carriers with the same band
-                new_configs[bts_index].band = band
-                bts_index += 1
-
-        # Get the bw for each carrier
-        # This is an optional parameter, by default the maximum bandwidth for
-        # each band will be selected.
-
-        if self.PARAM_BW not in parameters:
-            raise ValueError(
-                "The config dictionary must include the '{}' key.".format(
-                    self.PARAM_BW))
-
-        values = parameters[self.PARAM_BW]
-
-        bts_index = 0
-
-        for ca in ca_configs:
-
-            band = int(ca[:-1])
-            ca_class = ca[-1]
-
-            if values:
-                bw = int(values[bts_index])
-            else:
-                bw = max(self.allowed_bandwidth_dictionary[band])
-
-            new_configs[bts_index].bandwidth = bw
-            bts_index += 1
-
-            if ca_class.upper() == 'C':
-
-                new_configs[bts_index].bandwidth = bw
-
-                # Calculate the channel number for the second carrier to be
-                # contiguous to the first one
-                new_configs[bts_index].dl_channel = int(
-                    self.LOWEST_DL_CN_DICTIONARY[int(band)] + bw * 10 - 2)
-
-                bts_index += 1
-
-        # Get the MIMO mode for each carrier
-
-        if self.PARAM_MIMO not in parameters:
-            raise ValueError(
-                "The key '{}' has to be included in the config dictionary "
-                "with a list including the MIMO mode for each carrier.".format(
-                    self.PARAM_MIMO))
-
-        mimo_values = parameters[self.PARAM_MIMO]
-
-        if len(mimo_values) != self.num_carriers:
-            raise ValueError(
-                "The value of '{}' must be a list of MIMO modes with a length "
-                "equal to the number of carriers.".format(self.PARAM_MIMO))
-
-        for bts_index in range(self.num_carriers):
-
-            # Parse and set the requested MIMO mode
-
-            for mimo_mode in LteSimulation.MimoMode:
-                if mimo_values[bts_index] == mimo_mode.value:
-                    requested_mimo = mimo_mode
-                    break
-            else:
+        new_cell_list = []
+        for cell in parameters:
+            if self.PARAM_BAND not in cell:
                 raise ValueError(
-                    "The mimo mode must be one of %s." %
-                    {elem.value
-                     for elem in LteSimulation.MimoMode})
+                    "The configuration dictionary must include a key '{}' with "
+                    "the required band number.".format(self.PARAM_BAND))
 
-            if (requested_mimo == LteSimulation.MimoMode.MIMO_4x4
-                    and not self.simulator.LTE_SUPPORTS_4X4_MIMO):
-                raise ValueError("The test requires 4x4 MIMO, but that is not "
-                                 "supported by the MD8475A callbox.")
+            band = cell[self.PARAM_BAND]
 
-            new_configs[bts_index].mimo_mode = requested_mimo
+            if isinstance(band, str) and not band.isdigit():
+                ca_class = band[-1].upper()
+                band_num = int(band[:-1])
 
-            # Parse and set the requested TM
-            # This is an optional parameter, by the default value depends on the
-            # MIMO mode for each carrier
-            if self.PARAM_TM in parameters:
-                tm_values = parameters[self.PARAM_TM]
-                if len(tm_values) < bts_index + 1:
-                    raise ValueError(
-                        'The number of elements in the transmission mode list '
-                        'must be equal to the number of carriers.')
-                for tm in LteSimulation.TransmissionMode:
-                    if tm_values[bts_index] == tm.value[2:]:
-                        requested_tm = tm
-                        break
+                if ca_class in ['A', 'C']:
+                    # Remove the CA class label and add the cell
+                    cell[self.PARAM_BAND].band = band_num
+                    new_cell_list.append(cell)
+                elif ca_class == 'B':
+                    raise RuntimeError('Class B LTE CA not supported.')
                 else:
-                    raise ValueError(
-                        "The TM must be one of %s." %
-                        {elem.value
-                         for elem in LteSimulation.MimoMode})
+                    raise ValueError('Invalid band value: ' + band)
+
+                # Class C means that there are two contiguous carriers
+                if ca_class == 'C':
+                    new_cell_list.append(cell)
+                    bw = int(cell[self.PARAM_BW])
+                    new_cell_list[-1].dl_earfcn = int(
+                        self.LOWEST_DL_CN_DICTIONARY[band_num] + bw * 10 - 2)
             else:
-                # Provide default values if the TM parameter is not set
-                if requested_mimo == LteSimulation.MimoMode.MIMO_1x1:
-                    requested_tm = LteSimulation.TransmissionMode.TM1
-                else:
-                    requested_tm = LteSimulation.TransmissionMode.TM3
+                # The band is just a number, so just add it to the list.
+                new_cell_list.append(cell)
 
-            new_configs[bts_index].transmission_mode = requested_tm
+        self.simulator.set_band_combination(
+            [c[self.PARAM_BAND] for c in new_cell_list])
 
-            self.log.info("Cell {} will be set to {} and {} MIMO.".format(
-                bts_index + 1, requested_tm.value, requested_mimo.value))
+        self.num_carriers = len(new_cell_list)
 
-        # Get uplink power
+        # Setup the base station with the obtained configuration and then save
+        # these parameters in the current configuration object
+        for bts_index in range(self.num_carriers):
+            cell_config = self.configure_lte_cell(parameters[bts_index])
+            self.simulator.configure_bts(cell_config, bts_index)
+            self.bts_configs[bts_index].incorporate(cell_config)
 
-        ul_power = self.get_uplink_power_from_parameters(parameters)
+        # Now that the band is set, calibrate the link if necessary
+        self.load_pathloss_if_required()
+
+        # Get uplink power from primary carrier
+        ul_power = self.get_uplink_power_from_parameters(parameters[0])
 
         # Power is not set on the callbox until after the simulation is
         # started. Saving this value in a variable for later
         self.sim_ul_power = ul_power
 
-        # Get downlink power
-
-        dl_power = self.get_downlink_power_from_parameters(parameters)
+        # Get downlink power from primary carrier
+        dl_power = self.get_downlink_power_from_parameters(parameters[0])
 
         # Power is not set on the callbox until after the simulation is
         # started. Saving this value in a variable for later
         self.sim_dl_power = dl_power
 
-        # Setup scheduling mode
-        if self.PARAM_SCHEDULING not in parameters:
-            scheduling = LteSimulation.SchedulingMode.STATIC
-            self.log.warning(
-                "Key '{}' is not set in the config dictionary. Setting to "
-                "{} by default.".format(scheduling.value,
-                                        self.PARAM_SCHEDULING))
-        else:
-            for scheduling_mode in LteSimulation.SchedulingMode:
-                if (parameters[self.PARAM_SCHEDULING].upper() ==
-                        scheduling_mode.value):
-                    scheduling = scheduling_mode
-                    break
-            else:
-                raise ValueError(
-                    "Key '{}' must have a one of the following values: {}.".
-                    format(
-                        self.PARAM_SCHEDULING,
-                        {elem.value
-                         for elem in LteSimulation.SchedulingMode}))
-
-        for bts_index in range(self.num_carriers):
-            new_configs[bts_index].scheduling_mode = scheduling
-
-        if scheduling == LteSimulation.SchedulingMode.STATIC:
-            if self.PARAM_PATTERN not in parameters:
-                self.log.warning(
-                    "The '{}' key was not set, using 100% RBs for both "
-                    "DL and UL. To set the percentages of total RBs include "
-                    "the '{}' key with a list of two ints indicating downlink and uplink percentages."
-                    .format(self.PARAM_PATTERN, self.PARAM_PATTERN))
-                dl_pattern = 100
-                ul_pattern = 100
-            else:
-                values = parameters[self.PARAM_PATTERN]
-                dl_pattern = int(values[0])
-                ul_pattern = int(values[1])
-
-            if (dl_pattern, ul_pattern) not in [(0, 100), (100, 0),
-                                                (100, 100)]:
-                raise ValueError(
-                    "Only full RB allocation for DL or UL is supported in CA "
-                    "sims. The allowed combinations are 100/0, 0/100 and "
-                    "100/100.")
-
-            for bts_index in range(self.num_carriers):
-
-                # Look for a DL MCS configuration in the test parameters. If it
-                # is not present, use a default value.
-                if self.PARAM_DL_MCS in parameters:
-                    mcs_dl = int(parameters[self.PARAM_DL_MCS])
-                else:
-                    self.log.warning(
-                        'The config dictionary does not include the {} key. '
-                        'Setting to the max value by default'.format(
-                            self.PARAM_DL_MCS))
-
-                    if new_configs[bts_index].dl_256_qam_enabled and \
-                        new_configs[bts_index].bandwidth == 1.4:
-                        mcs_dl = 26
-                    elif (not new_configs[bts_index].dl_256_qam_enabled
-                          and new_configs[bts_index].mac_padding
-                          and new_configs[bts_index].bandwidth != 1.4):
-                        mcs_dl = 28
-                    else:
-                        mcs_dl = 27
-
-                # Look for an UL MCS configuration in the test parameters. If it
-                # is not present, use a default value.
-                if self.PARAM_UL_MCS in parameters:
-                    mcs_ul = int(parameters[self.PARAM_UL_MCS])
-                else:
-                    self.log.warning(
-                        'The config dictionary does not include the {} key. '
-                        'Setting to the max value by default'.format(
-                            self.PARAM_UL_MCS))
-
-                    if new_configs[bts_index].ul_64_qam_enabled:
-                        mcs_ul = 28
-                    else:
-                        mcs_ul = 23
-
-                dl_rbs, ul_rbs = self.allocation_percentages_to_rbs(
-                    new_configs[bts_index].bandwidth,
-                    new_configs[bts_index].transmission_mode, dl_pattern,
-                    ul_pattern)
-
-                new_configs[bts_index].dl_rbs = dl_rbs
-                new_configs[bts_index].ul_rbs = ul_rbs
-                new_configs[bts_index].dl_mcs = mcs_dl
-                new_configs[bts_index].ul_mcs = mcs_ul
-
-        # Setup the base stations with the obtained configurations and then save
-        # these parameters in the current configuration objects
-        for bts_index in range(len(new_configs)):
-            self.simulator.configure_bts(new_configs[bts_index], bts_index)
-            self.bts_configs[bts_index].incorporate(new_configs[bts_index])
-
         # Now that the band is set, calibrate the link for the PCC if necessary
         self.load_pathloss_if_required()
 
diff --git a/acts/framework/acts/controllers/cellular_lib/LteSimulation.py b/acts/framework/acts/controllers/cellular_lib/LteSimulation.py
index 71231d6..defd5d4 100644
--- a/acts/framework/acts/controllers/cellular_lib/LteSimulation.py
+++ b/acts/framework/acts/controllers/cellular_lib/LteSimulation.py
@@ -525,6 +525,44 @@
         Args:
             parameters: a configuration dictionary
         """
+        # Setup band
+        if self.PARAM_BAND not in parameters:
+            raise ValueError(
+                "The configuration dictionary must include a key '{}' with "
+                "the required band number.".format(self.PARAM_BAND))
+
+        self.simulator.set_band_combination([parameters[self.PARAM_BAND]])
+
+        new_config = self.configure_lte_cell(parameters)
+
+        # Get uplink power
+        ul_power = self.get_uplink_power_from_parameters(parameters)
+
+        # Power is not set on the callbox until after the simulation is
+        # started. Saving this value in a variable for later
+        self.sim_ul_power = ul_power
+
+        # Get downlink power
+        dl_power = self.get_downlink_power_from_parameters(parameters)
+
+        # Power is not set on the callbox until after the simulation is
+        # started. Saving this value in a variable for later
+        self.sim_dl_power = dl_power
+
+        # Setup the base station with the obtained configuration and then save
+        # these parameters in the current configuration object
+        self.simulator.configure_bts(new_config)
+        self.primary_config.incorporate(new_config)
+
+        # Now that the band is set, calibrate the link if necessary
+        self.load_pathloss_if_required()
+
+    def configure_lte_cell(self, parameters):
+        """ Configures an LTE cell using a dictionary of parameters.
+
+        Args:
+            parameters: a configuration dictionary
+        """
         # Instantiate a new configuration object
         new_config = self.BtsConfig()
 
@@ -535,7 +573,6 @@
                 "the required band number.".format(self.PARAM_BAND))
 
         new_config.band = parameters[self.PARAM_BAND]
-        self.simulator.set_band_combination([new_config.band])
 
         if not self.PARAM_DL_EARFCN in parameters:
             band = int(new_config.band)
@@ -801,29 +838,7 @@
                     'The {} key has to be followed by the paging cycle '
                     'duration in milliseconds.'.format(self.PARAM_PAGING))
 
-        # Get uplink power
-
-        ul_power = self.get_uplink_power_from_parameters(parameters)
-
-        # Power is not set on the callbox until after the simulation is
-        # started. Saving this value in a variable for later
-        self.sim_ul_power = ul_power
-
-        # Get downlink power
-
-        dl_power = self.get_downlink_power_from_parameters(parameters)
-
-        # Power is not set on the callbox until after the simulation is
-        # started. Saving this value in a variable for later
-        self.sim_dl_power = dl_power
-
-        # Setup the base station with the obtained configuration and then save
-        # these parameters in the current configuration object
-        self.simulator.configure_bts(new_config)
-        self.primary_config.incorporate(new_config)
-
-        # Now that the band is set, calibrate the link if necessary
-        self.load_pathloss_if_required()
+        return new_config
 
     def calibrated_downlink_rx_power(self, bts_config, rsrp):
         """ LTE simulation overrides this method so that it can convert from
diff --git a/acts/framework/acts/controllers/openwrt_lib/network_settings.py b/acts/framework/acts/controllers/openwrt_lib/network_settings.py
index efbd590..ca1b212 100644
--- a/acts/framework/acts/controllers/openwrt_lib/network_settings.py
+++ b/acts/framework/acts/controllers/openwrt_lib/network_settings.py
@@ -28,11 +28,12 @@
 SERVICE_IPSEC = "ipsec"
 SERVICE_XL2TPD = "xl2tpd"
 SERVICE_ODHCPD = "odhcpd"
-SERVICE_NODOGSPLASH = "nodogsplash"
+SERVICE_OPENNDS = "opennds"
+SERVICE_UHTTPD = "uhttpd"
 PPTP_PACKAGE = "pptpd kmod-nf-nathelper-extra"
 L2TP_PACKAGE = "strongswan-full openssl-util xl2tpd"
 NAT6_PACKAGE = "ip6tables kmod-ipt-nat6"
-CAPTIVE_PORTAL_PACKAGE = "nodogsplash"
+CAPTIVE_PORTAL_PACKAGE = "opennds php7-cli php7-mod-openssl php7-cgi php7"
 MDNS_PACKAGE = "avahi-utils avahi-daemon-service-http avahi-daemon-service-ssh libavahi-client avahi-dbus-daemon"
 STUNNEL_CONFIG_PATH = "/etc/stunnel/DoTServer.conf"
 HISTORY_CONFIG_PATH = "/etc/dirty_configs"
@@ -188,13 +189,28 @@
         return False
 
     def path_exists(self, abs_path):
-        """Check if dir exist on OpenWrt."""
+        """Check if dir exist on OpenWrt.
+
+        Args:
+            abs_path: absolutely path for create folder.
+        """
         try:
             self.ssh.run("ls %s" % abs_path)
         except:
             return False
         return True
 
+    def create_folder(self, abs_path):
+        """If dir not exist, create it.
+
+        Args:
+            abs_path: absolutely path for create folder.
+        """
+        if not self.path_exists(abs_path):
+            self.ssh.run("mkdir %s" % abs_path)
+        else:
+            self.log.info("%s already existed." %abs_path)
+
     def count(self, config, key):
         """Count in uci config.
 
@@ -849,6 +865,7 @@
             tcpdump_file_name: tcpdump file name on OpenWrt.
             pid: tcpdump process id.
         """
+        self.package_install("tcpdump")
         if not self.path_exists(TCPDUMP_DIR):
             self.ssh.run("mkdir %s" % TCPDUMP_DIR)
         tcpdump_file_name = "openwrt_%s_%s.pcap" % (test_name,
@@ -920,15 +937,54 @@
         self.service_manager.need_restart(SERVICE_FIREWALL)
         self.commit_changes()
 
-    def setup_captive_portal(self):
+    def setup_captive_portal(self, fas_fdqn,fas_port=2080):
+        """Create captive portal with Forwarding Authentication Service.
+
+        Args:
+             fas_fdqn: String for captive portal page's fdqn add to local dns server.
+             fas_port: Port for captive portal page.
+        """
         self.package_install(CAPTIVE_PORTAL_PACKAGE)
-        self.config.add("setup_captive_portal")
-        self.service_manager.need_restart(SERVICE_NODOGSPLASH)
+        self.config.add("setup_captive_portal %s" % fas_port)
+        self.ssh.run("uci set opennds.@opennds[0].fas_secure_enabled=2")
+        self.ssh.run("uci set opennds.@opennds[0].gatewayport=2050")
+        self.ssh.run("uci set opennds.@opennds[0].fasport=%s" % fas_port)
+        self.ssh.run("uci set opennds.@opennds[0].fasremotefqdn=%s" % fas_fdqn)
+        self.ssh.run("uci set opennds.@opennds[0].faspath=\"/nds/fas-aes.php\"")
+        self.ssh.run("uci set opennds.@opennds[0].faskey=1234567890")
+        self.service_manager.need_restart(SERVICE_OPENNDS)
+        # Config uhttpd
+        self.ssh.run("uci set uhttpd.main.interpreter=.php=/usr/bin/php-cgi")
+        self.ssh.run("uci add_list uhttpd.main.listen_http=0.0.0.0:%s" % fas_port)
+        self.ssh.run("uci add_list uhttpd.main.listen_http=[::]:%s" % fas_port)
+        self.service_manager.need_restart(SERVICE_UHTTPD)
+        # cp fas-aes.php
+        self.create_folder("/www/nds/")
+        self.ssh.run("cp /etc/opennds/fas-aes.php /www/nds")
+        # Add fdqn
+        self.add_resource_record(fas_fdqn, LOCALHOST)
         self.commit_changes()
 
-    def remove_cpative_portal(self):
+    def remove_cpative_portal(self, fas_port=2080):
+        """Remove captive portal.
+
+        Args:
+             fas_port: Port for captive portal page.
+        """
+        # Remove package
         self.package_remove(CAPTIVE_PORTAL_PACKAGE)
-        self.config.discard("setup_captive_portal")
+        # Clean up config
+        self.ssh.run("rm /etc/config/opennds")
+        # Remove fdqn
+        self.clear_resource_record()
+        # Restore uhttpd
+        self.ssh.run("uci del uhttpd.main.interpreter")
+        self.ssh.run("uci del_list uhttpd.main.listen_http=\'0.0.0.0:%s\'" % fas_port)
+        self.ssh.run("uci del_list uhttpd.main.listen_http=\'[::]:%s\'" % fas_port)
+        self.service_manager.need_restart(SERVICE_UHTTPD)
+        # Clean web root
+        self.ssh.run("rm -r /www/nds")
+        self.config.discard("setup_captive_portal %s" % fas_port)
         self.commit_changes()
 
 
diff --git a/acts_tests/acts_contrib/test_utils/net/connectivity_test_utils.py b/acts_tests/acts_contrib/test_utils/net/connectivity_test_utils.py
index d35fe04..1b547e3 100644
--- a/acts_tests/acts_contrib/test_utils/net/connectivity_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/net/connectivity_test_utils.py
@@ -104,7 +104,14 @@
     msg = "Failed to receive confirmation of stopping socket keepalive"
     return _listen_for_keepalive_event(ad, key, msg, "Stopped")
 
+
 def set_private_dns(ad, dns_mode, hostname=None):
+    """ Set private DNS mode and DNS server hostname on DUT
+
+    :param ad: Device under test (DUT)
+    :param dns_mode: DNS mode, including OFF, OPPORTUNISTIC, STRICT
+    :param hostname: DNS server hostname
+    """
     """ Set private DNS mode on dut """
     if dns_mode == cconst.PRIVATE_DNS_MODE_OFF:
         ad.droid.setPrivateDnsMode(False)
@@ -114,6 +121,3 @@
     mode = ad.droid.getPrivateDnsMode()
     host = ad.droid.getPrivateDnsSpecifier()
     ad.log.info("DNS mode is %s and DNS server is %s" % (mode, host))
-    asserts.assert_true(dns_mode == mode and host == hostname,
-                        "Failed to set DNS mode to %s and DNS to %s" % \
-                        (dns_mode, hostname))
diff --git a/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py b/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
index 1481e95..6e62263 100644
--- a/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
+++ b/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
@@ -464,6 +464,7 @@
                                 measure_after_seconds=self.mon_info.offset,
                                 hz=self.mon_info.freq)
         self.power_monitor.measure(measurement_args=measurement_args,
+                                   measurement_name=self.test_name,
                                    start_time=device_to_host_offset,
                                    monsoon_output_path=data_path)
         self.power_monitor.release_resources()
diff --git a/acts_tests/acts_contrib/test_utils/tel/tel_parse_utils.py b/acts_tests/acts_contrib/test_utils/tel/tel_parse_utils.py
index dedfc24..e4366cd 100644
--- a/acts_tests/acts_contrib/test_utils/tel/tel_parse_utils.py
+++ b/acts_tests/acts_contrib/test_utils/tel/tel_parse_utils.py
@@ -14,12 +14,15 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import time
-import random
+import copy
 import re
 import statistics
 
+from acts import signals
+from acts_contrib.test_utils.tel.tel_defines import INVALID_SUB_ID
 from acts_contrib.test_utils.tel.tel_subscription_utils import get_slot_index_from_data_sub_id
+from acts_contrib.test_utils.tel.tel_subscription_utils import get_slot_index_from_voice_sub_id
+from acts_contrib.test_utils.tel.tel_subscription_utils import get_subid_from_slot_index
 
 SETUP_DATA_CALL = 'SETUP_DATA_CALL'
 SETUP_DATA_CALL_REQUEST = '> SETUP_DATA_CALL'
@@ -47,6 +50,19 @@
 WHI_IWLAN_DEACTIVATE_DATA_CALL_REQUEST = r'IwlanDataService\[\d\]: Deactivate data call'
 WHI_IWLAN_DEACTIVATE_DATA_CALL_RESPONSE = r'IwlanDataService\[\d\]: Tunnel closed!'
 
+ON_ENABLE_APN_IMS_SLOT0 = 'DCT-C-0 : onEnableApn: apnType=ims, request type=NORMAL'
+ON_ENABLE_APN_IMS_SLOT1 = 'DCT-C-1 : onEnableApn: apnType=ims, request type=NORMAL'
+ON_ENABLE_APN_IMS_HANDOVER_SLOT0 = 'DCT-C-0 : onEnableApn: apnType=ims, request type=HANDOVER'
+ON_ENABLE_APN_IMS_HANDOVER_SLOT1 = 'DCT-C-1 : onEnableApn: apnType=ims, request type=HANDOVER'
+RADIO_ON_4G_SLOT0 = r'GsmCdmaPhone: \[0\] Event EVENT_RADIO_ON Received'
+RADIO_ON_4G_SLOT1 = r'GsmCdmaPhone: \[1\] Event EVENT_RADIO_ON Received'
+RADIO_ON_IWLAN = 'Switching to new default network.*WIFI CONNECTED'
+WIFI_OFF = 'setWifiEnabled.*enable=false'
+ON_IMS_MM_TEL_CONNECTED_4G_SLOT0 = r'ImsPhone: \[0\].*onImsMmTelConnected imsRadioTech=WWAN'
+ON_IMS_MM_TEL_CONNECTED_4G_SLOT1 = r'ImsPhone: \[1\].*onImsMmTelConnected imsRadioTech=WWAN'
+ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0 = r'ImsPhone: \[0\].*onImsMmTelConnected imsRadioTech=WLAN'
+ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1 = r'ImsPhone: \[1\].*onImsMmTelConnected imsRadioTech=WLAN'
+
 def print_nested_dict(ad, d):
     divider = "------"
     for k, v in d.items():
@@ -1057,4 +1073,170 @@
     return (
         deactivate_data_call,
         deactivate_data_call_time_list,
-        avg_deactivate_data_call_time)
\ No newline at end of file
+        avg_deactivate_data_call_time)
+
+def parse_ims_reg(
+    ad,
+    search_intervals=None,
+    rat='4g',
+    reboot_or_apm='reboot',
+    slot=None):
+    """Search in logcat for lines containing messages about IMS registration.
+
+    Args:
+        ad: Android object
+        search_intervals: List. Only lines with time stamp in given time
+            intervals will be parsed.
+            E.g., [(begin_time1, end_time1), (begin_time2, end_time2)]
+            Both begin_time and end_time should be datetime object.
+        rat: "4g" for IMS over LTE or "iwlan" for IMS over Wi-Fi
+        reboot_or_apm: specify the scenario "reboot" or "apm"
+        slot: 0 for pSIM and 1 for eSIM
+
+    Returns:
+        (ims_reg, parsing_fail, avg_ims_reg_duration)
+
+        ims_reg: List of dictionaries containing found lines for start and
+            end time stamps. Each dict represents a cycle of the test.
+
+            [
+                {'start': message on start time stamp,
+                'end': message on end time stamp,
+                'duration': time difference between start and end}
+            ]
+        parsing_fail: List of dictionaries containing the cycle number and
+            missing messages of each failed cycle
+
+            [
+                'attempt': failed cycle number
+                'missing_msg' missing messages which should be found
+            ]
+        avg_ims_reg_duration: average of the duration in ims_reg
+
+    """
+    if slot is None:
+        slot = get_slot_index_from_voice_sub_id(ad)
+        ad.log.info('Default voice slot: %s', slot)
+    else:
+        if get_subid_from_slot_index(ad.log, ad, slot) == INVALID_SUB_ID:
+            ad.log.error('Slot %s is invalid.', slot)
+            raise signals.TestFailure('Failed',
+                extras={'fail_reason': 'Slot %s is invalid.' % slot})
+
+        ad.log.info('Assigned slot: %s', slot)
+
+    start_command = {
+        'reboot': {
+            '0': {'4g': ON_ENABLE_APN_IMS_SLOT0,
+                'iwlan': ON_ENABLE_APN_IMS_HANDOVER_SLOT0 + '\|' + ON_ENABLE_APN_IMS_SLOT0},
+            '1': {'4g': ON_ENABLE_APN_IMS_SLOT1,
+                'iwlan': ON_ENABLE_APN_IMS_HANDOVER_SLOT1 + '\|' + ON_ENABLE_APN_IMS_SLOT1}
+        },
+        'apm':{
+            '0': {'4g': RADIO_ON_4G_SLOT0, 'iwlan': RADIO_ON_IWLAN},
+            '1': {'4g': RADIO_ON_4G_SLOT1, 'iwlan': RADIO_ON_IWLAN}
+        },
+        'wifi_off':{
+            '0': {'4g': WIFI_OFF, 'iwlan': WIFI_OFF},
+            '1': {'4g': WIFI_OFF, 'iwlan': WIFI_OFF}
+        },
+    }
+
+    end_command = {
+        '0': {'4g': ON_IMS_MM_TEL_CONNECTED_4G_SLOT0,
+            'iwlan': ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0},
+        '1': {'4g': ON_IMS_MM_TEL_CONNECTED_4G_SLOT1,
+            'iwlan': ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1}
+    }
+
+    ad.log.info('====== Start to search logcat ======')
+    logcat = ad.search_logcat('%s\|%s' % (
+        start_command[reboot_or_apm][str(slot)][rat],
+        end_command[str(slot)][rat]))
+
+    if not logcat:
+        raise signals.TestFailure('Failed',
+            extras={'fail_reason': 'No line matching the given pattern can '
+            'be found in logcat.'})
+
+    for msg in logcat:
+        ad.log.info(msg["log_message"])
+
+    ims_reg = []
+    ims_reg_duration_list = []
+    parsing_fail = []
+
+    start_command['reboot'] = {
+        '0': {'4g': ON_ENABLE_APN_IMS_SLOT0,
+            'iwlan': ON_ENABLE_APN_IMS_HANDOVER_SLOT0 + '|' + ON_ENABLE_APN_IMS_SLOT0},
+        '1': {'4g': ON_ENABLE_APN_IMS_SLOT1,
+            'iwlan': ON_ENABLE_APN_IMS_HANDOVER_SLOT1 + '|' + ON_ENABLE_APN_IMS_SLOT1}
+    }
+
+    keyword_dict = {
+        'start': start_command[reboot_or_apm][str(slot)][rat],
+        'end': end_command[str(slot)][rat]
+    }
+
+    for attempt, interval in enumerate(search_intervals):
+        if isinstance(interval, list):
+            try:
+                begin_time, end_time = interval
+            except Exception as e:
+                ad.log.error(e)
+                continue
+
+            ad.log.info('Parsing begin time: %s', begin_time)
+            ad.log.info('Parsing end time: %s', end_time)
+
+            temp_keyword_dict = copy.deepcopy(keyword_dict)
+            for line in logcat:
+                if begin_time and line['datetime_obj'] < begin_time:
+                    continue
+
+                if end_time and line['datetime_obj'] > end_time:
+                    break
+
+                for key in temp_keyword_dict:
+                    if temp_keyword_dict[key] and not isinstance(
+                        temp_keyword_dict[key], dict):
+                        res = re.findall(
+                            temp_keyword_dict[key], line['log_message'])
+                        if res:
+                            ad.log.info('Found: %s', line['log_message'])
+                            temp_keyword_dict[key] = {
+                                'message': line['log_message'],
+                                'time_stamp': line['datetime_obj']}
+                            break
+
+            for key in temp_keyword_dict:
+                if temp_keyword_dict[key] == keyword_dict[key]:
+                    ad.log.error(
+                        '"%s" is missing in cycle %s.',
+                        keyword_dict[key],
+                        attempt)
+                    parsing_fail.append({
+                        'attempt': attempt,
+                        'missing_msg': keyword_dict[key]})
+            try:
+                ims_reg_duration = (
+                    temp_keyword_dict['end'][
+                        'time_stamp'] - temp_keyword_dict[
+                            'start'][
+                                'time_stamp']).total_seconds()
+                ims_reg_duration_list.append(ims_reg_duration)
+                ims_reg.append({
+                    'start': temp_keyword_dict['start'][
+                        'message'],
+                    'end': temp_keyword_dict['end'][
+                        'message'],
+                    'duration': ims_reg_duration})
+            except Exception as e:
+                ad.log.error(e)
+
+    try:
+        avg_ims_reg_duration = statistics.mean(ims_reg_duration_list)
+    except:
+        avg_ims_reg_duration = None
+
+    return ims_reg, parsing_fail, avg_ims_reg_duration
\ No newline at end of file
diff --git a/acts_tests/acts_contrib/test_utils/tel/tel_subscription_utils.py b/acts_tests/acts_contrib/test_utils/tel/tel_subscription_utils.py
index 39e0370..bab91a6 100644
--- a/acts_tests/acts_contrib/test_utils/tel/tel_subscription_utils.py
+++ b/acts_tests/acts_contrib/test_utils/tel/tel_subscription_utils.py
@@ -592,3 +592,38 @@
         if info['subscriptionId'] == data_sub_id:
             return info['simSlotIndex']
     return INVALID_SUB_ID
+
+def get_slot_index_from_voice_sub_id(ad):
+    """Get slot index from the current voice sub ID.
+
+    Args:
+        ad: android object
+
+    Returns:
+        0: pSIM
+        1: eSIM
+        INVALID_SUB_ID (-1): if no sub ID is equal to current voice sub ID.
+    """
+    voice_sub_id = get_incoming_voice_sub_id(ad)
+    sub_info = ad.droid.subscriptionGetAllSubInfoList()
+    for info in sub_info:
+        if info['subscriptionId'] == voice_sub_id:
+            return info['simSlotIndex']
+    return INVALID_SUB_ID
+
+def get_all_sub_id(ad):
+    """Return all valid subscription IDs.
+
+    Args:
+        ad: Android object
+
+    Returns:
+        List containing all valid subscription IDs.
+    """
+    sub_id_list = []
+    sub_info = ad.droid.subscriptionGetAllSubInfoList()
+    for info in sub_info:
+        if info['simSlotIndex'] != INVALID_SUB_ID:
+            sub_id_list.append(info['subscriptionId'])
+
+    return sub_id_list
\ No newline at end of file
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py
index 2e3f336..1729a7c 100755
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py
@@ -744,6 +744,7 @@
         "Failed to remove these configured Wi-Fi networks: %s" % networks)
 
 
+
 def toggle_airplane_mode_on_and_off(ad):
     """Turn ON and OFF Airplane mode.
 
diff --git a/acts_tests/tests/google/fuchsia/dhcp/Dhcpv4InteropTest.py b/acts_tests/tests/google/fuchsia/dhcp/Dhcpv4InteropTest.py
index d23d226..783f9d0 100644
--- a/acts_tests/tests/google/fuchsia/dhcp/Dhcpv4InteropTest.py
+++ b/acts_tests/tests/google/fuchsia/dhcp/Dhcpv4InteropTest.py
@@ -14,6 +14,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import ipaddress
 import itertools
 import time
 import re
@@ -25,6 +26,7 @@
 from acts.controllers.ap_lib import hostapd_constants
 from acts.controllers.ap_lib.hostapd_security import Security
 from acts.controllers.ap_lib.hostapd_utils import generate_random_password
+from acts.controllers.utils_lib.commands import ip
 from acts_contrib.test_utils.abstract_devices.wlan_device import create_wlan_device
 from acts_contrib.test_utils.abstract_devices.wlan_device_lib.AbstractDeviceWlanDeviceBaseTest import AbstractDeviceWlanDeviceBaseTest
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
@@ -100,16 +102,19 @@
         target_security = hostapd_constants.SECURITY_STRING_TO_DEFAULT_TARGET_SECURITY.get(
             security_mode)
 
-        setup_ap(access_point=self.access_point,
-                 profile_name='whirlwind',
-                 mode=hostapd_constants.MODE_11N_MIXED,
-                 channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
-                 n_capabilities=[],
-                 ac_capabilities=[],
-                 force_wmm=True,
-                 ssid=ssid,
-                 security=security_profile,
-                 password=password)
+        ap_ids = setup_ap(access_point=self.access_point,
+                          profile_name='whirlwind',
+                          mode=hostapd_constants.MODE_11N_MIXED,
+                          channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
+                          n_capabilities=[],
+                          ac_capabilities=[],
+                          force_wmm=True,
+                          ssid=ssid,
+                          security=security_profile,
+                          password=password)
+
+        if len(ap_ids) > 1:
+            raise Exception("Expected only one SSID on AP")
 
         configured_subnets = self.access_point.get_configured_subnets()
         if len(configured_subnets) > 1:
@@ -125,6 +130,7 @@
             'target_security': target_security,
             'ip': router_ip,
             'network': network,
+            'id': ap_ids[0],
         }
 
     def device_can_ping(self, dest_ip):
@@ -338,6 +344,93 @@
             dhcp_logs + "\n")
 
 
+class Dhcpv4DuplicateAddressTest(Dhcpv4InteropFixture):
+    def setup_test(self):
+        super().setup_test()
+        self.extra_addresses = []
+        self.ap_params = self.setup_ap()
+        self.ap_ip_cmd = ip.LinuxIpCommand(self.access_point.ssh)
+
+    def teardown_test(self):
+        super().teardown_test()
+        for ip in self.extra_addresses:
+            self.ap_ip_cmd.remove_ipv4_address(self.ap_params['id'], ip)
+            pass
+
+    def test_duplicate_address_assignment(self):
+        """It's possible for a DHCP server to assign an address that already exists on the network.
+        DHCP clients are expected to perform a "gratuitous ARP" of the to-be-assigned address, and
+        refuse to assign that address. Clients should also recover by asking for a different
+        address.
+        """
+        # Modify subnet to hold fewer addresses.
+        # A '/29' has 8 addresses (6 usable excluding router / broadcast)
+        subnet = next(self.ap_params['network'].subnets(new_prefix=29))
+        subnet_conf = dhcp_config.Subnet(
+            subnet=subnet,
+            router=self.ap_params['ip'],
+            # When the DHCP server is considering dynamically allocating an IP address to a client,
+            # it first sends an ICMP Echo request (a ping) to the address being assigned. It waits
+            # for a second, and if no ICMP Echo response has been heard, it assigns the address.
+            # If a response is heard, the lease is abandoned, and the server does not respond to
+            # the client.
+            # The ping-check configuration parameter can be used to control checking - if its value
+            # is false, no ping check is done.
+            additional_parameters=[('ping-check', 'false')])
+        dhcp_conf = dhcp_config.DhcpConfig(subnets=[subnet_conf])
+        self.access_point.start_dhcp(dhcp_conf=dhcp_conf)
+
+        # Add each of the usable IPs as an alias for the router's interface, such that the router
+        # will respond to any pings on it.
+        for ip in subnet.hosts():
+            self.ap_ip_cmd.add_ipv4_address(self.ap_params['id'], ip)
+            # Ensure we remove the address in self.teardown_test() even if the test fails
+            self.extra_addresses.append(ip)
+
+        self.connect(ap_params=self.ap_params)
+        with asserts.assert_raises(ConnectionError):
+            self.get_device_ipv4_addr()
+
+        # Per spec, the flow should be:
+        # Discover -> Offer -> Request -> Ack -> client optionally performs DAD
+        dhcp_logs = self.access_point.get_dhcp_logs()
+        for expected_message in [
+                r'DHCPDISCOVER from \S+',
+                r'DHCPOFFER on [0-9.]+ to \S+',
+                r'DHCPREQUEST for [0-9.]+',
+                r'DHCPACK on [0-9.]+',
+                r'DHCPDECLINE of [0-9.]+ from \S+ via .*: abandoned',
+                r'Abandoning IP address [0-9.]+: declined',
+        ]:
+            asserts.assert_true(
+                re.search(expected_message, dhcp_logs),
+                f'Did not find expected message ({expected_message}) in dhcp logs: {dhcp_logs}'
+                + "\n")
+
+        # Remove each of the IP aliases.
+        # Note: this also removes the router's address (e.g. 192.168.1.1), so pinging the
+        # router after this will not work.
+        while self.extra_addresses:
+            self.ap_ip_cmd.remove_ipv4_address(self.ap_params['id'],
+                                               self.extra_addresses.pop())
+
+        # Now, we should get an address successfully
+        ip = self.get_device_ipv4_addr()
+        dhcp_logs = self.access_point.get_dhcp_logs()
+
+        expected_string = f'DHCPREQUEST for {ip}'
+        asserts.assert_true(
+            dhcp_logs.count(expected_string) >= 1,
+            f'Incorrect count of DHCP Requests ("{expected_string}") in logs: '
+            + dhcp_logs + "\n")
+
+        expected_string = f'DHCPACK on {ip}'
+        asserts.assert_true(
+            dhcp_logs.count(expected_string) >= 1,
+            f'Incorrect count of DHCP Acks ("{expected_string}") in logs: ' +
+            dhcp_logs + "\n")
+
+
 class Dhcpv4InteropCombinatorialOptionsTest(Dhcpv4InteropFixture):
     """DhcpV4 tests which validate combinations of DHCP options."""
     OPTION_DOMAIN_NAME = [('domain-name', 'example.invalid'),
diff --git a/acts_tests/tests/google/net/CaptivePortalTest.py b/acts_tests/tests/google/net/CaptivePortalTest.py
index 9cf957c..c44ff9b 100644
--- a/acts_tests/tests/google/net/CaptivePortalTest.py
+++ b/acts_tests/tests/google/net/CaptivePortalTest.py
@@ -31,6 +31,7 @@
 ACCEPT_CONTINUE = "Accept and Continue"
 CONNECTED = "Connected"
 SIGN_IN_NOTIFICATION = "Sign in to network"
+FAS_FDQN = "netsplashpage.net"
 
 
 class CaptivePortalTest(WifiBaseTest):
@@ -60,7 +61,7 @@
             else:
                 self.configure_openwrt_ap_and_start(wpa_network=True)
                 self.wifi_network = self.openwrt.get_wifi_network()
-            self.openwrt.network_setting.setup_captive_portal()
+            self.openwrt.network_setting.setup_captive_portal(FAS_FDQN)
 
     def teardown_class(self):
         """Reset devices."""
@@ -105,7 +106,9 @@
                   return
         asserts.fail("Failed to get sign in notification")
 
-    def _verify_captive_portal(self, network, click_accept=ACCEPT_CONTINUE):
+    def _verify_captive_portal(self, network, user="username",
+                               mail="user@example.net",
+                               click_accept=ACCEPT_CONTINUE):
         """Connect to captive portal network using uicd workflow.
 
         Steps:
@@ -115,15 +118,24 @@
 
         Args:
             network: captive portal network to connect to
+            user: Option for captive portal login in
+            mail: Option for captive portal login in
             click_accept: Notification to select to accept captive portal
         """
         # connect to captive portal wifi network
         wutils.connect_to_wifi_network(
             self.dut, network, check_connectivity=False)
-
+        # Wait for captive portal detection.
+        time.sleep(10)
         # run ui automator
         self._verify_sign_in_notification()
         uutils.wait_and_click(self.dut, text="%s" % network["SSID"])
+        if uutils.has_element(self.dut, class_name="android.widget.EditText"):
+            uutils.wait_and_click(self.dut, class_name="android.widget.EditText")
+            self.dut.adb.shell("input text %s" % user)
+            self.dut.adb.shell("input keyevent 20")
+            self.dut.adb.shell("input text %s" % mail)
+            uutils.wait_and_click(self.dut, text="Accept Terms of Service")
         if uutils.has_element(self.dut, text="%s" % click_accept):
             uutils.wait_and_click(self.dut, text="%s" % click_accept)
 
@@ -250,7 +262,7 @@
             3. Verify connectivity
         """
         cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC)
-        self.openwrt.network_setting.service_manager.restart("nodogsplash")
+        self.openwrt.network_setting.service_manager.restart("opennds")
         self._verify_captive_portal(self.wifi_network, click_accept="Continue")
 
     @test_tracker_info(uuid="1419e36d-0303-44ba-bc60-4d707b45ef48")
@@ -263,7 +275,7 @@
             3. Verify connectivity
         """
         cutils.set_private_dns(self.dut, cconst.PRIVATE_DNS_MODE_OFF)
-        self.openwrt.network_setting.service_manager.restart("nodogsplash")
+        self.openwrt.network_setting.service_manager.restart("opennds")
         self._verify_captive_portal(self.wifi_network, click_accept="Continue")
 
     @test_tracker_info(uuid="5aae44ee-fa62-47b9-9b3d-8121f9f92da1")
@@ -278,5 +290,7 @@
         cutils.set_private_dns(self.dut,
                                cconst.PRIVATE_DNS_MODE_STRICT,
                                cconst.DNS_GOOGLE_HOSTNAME)
-        self.openwrt.network_setting.service_manager.restart("nodogsplash")
+        self.openwrt.network_setting.service_manager.restart("opennds")
         self._verify_captive_portal(self.wifi_network, click_accept="Continue")
+
+
diff --git a/acts_tests/tests/google/net/DnsOverTlsTest.py b/acts_tests/tests/google/net/DnsOverTlsTest.py
index 0c3feac..f581e39 100644
--- a/acts_tests/tests/google/net/DnsOverTlsTest.py
+++ b/acts_tests/tests/google/net/DnsOverTlsTest.py
@@ -186,6 +186,42 @@
         # reset wifi
         wutils.reset_wifi(self.dut)
 
+    def _test_invalid_private_dns(self, net, dns_mode, dns_hostname):
+        """Test private DNS with invalid hostname, which should failed the ping.
+
+        :param net: Wi-Fi network to connect to
+        :param dns_mode: private DNS mode
+        :param dns_hostname: private DNS hostname
+        :return:
+        """
+
+        cutils.set_private_dns(self.dut, dns_mode, dns_hostname)
+        if net:
+            wutils.start_wifi_connection_scan_and_ensure_network_found(
+                self.dut, net[SSID])
+            wutils.wifi_connect(
+                self.dut, net, assert_on_fail=False, check_connectivity=False)
+
+        self._start_tcp_dump(self.dut)
+
+        # ping hosts should NOT pass
+        ping_result = False
+        for host in self.ping_hosts:
+            self.log.info("Pinging %s" % host)
+            try:
+                ping_result = self.dut.droid.httpPing(host)
+            except:
+                pass
+            # Ping result should keep negative with invalid DNS,
+            # so once it's positive we should break, and the test should fail
+            if ping_result:
+                break
+
+        pcap_file = self._stop_tcp_dump(self.dut)
+        self._verify_dns_queries_over_tls(pcap_file, True)
+        wutils.reset_wifi(self.dut)
+        return ping_result
+
     @test_tracker_info(uuid="2957e61c-d333-45fb-9ff9-2250c9c8535a")
     def test_private_dns_mode_off_wifi_ipv4_only_network(self):
         """Verify private dns mode off on ipv4 only network.
@@ -539,6 +575,7 @@
         for host in self.ping_hosts:
             wutils.validate_connection(self.dut, host)
 
+
         # stop tcpdump on device
         pcap_file = self._stop_tcp_dump(self.dut)
 
@@ -547,18 +584,17 @@
 
     @test_tracker_info(uuid="af6e34f1-3ad5-4ab0-b3b9-53008aa08294")
     def test_private_dns_mode_strict_invalid_hostnames(self):
-        """Verify that invalid hostnames are not saved for strict mode.
+        """Verify that invalid hostnames are not able to ping for strict mode.
 
         Steps:
             1. Set private DNS to strict mode with invalid hostname
             2. Verify that invalid hostname is not saved
         """
         invalid_hostnames = ["!%@&!*", "12093478129", "9.9.9.9", "sdkfjhasdf"]
-        for hostname in invalid_hostnames:
-            cutils.set_private_dns(
-                self.dut, cconst.PRIVATE_DNS_MODE_STRICT, hostname)
-            mode = self.dut.droid.getPrivateDnsMode()
-            specifier = self.dut.droid.getPrivateDnsSpecifier()
-            asserts.assert_true(
-                mode == cconst.PRIVATE_DNS_MODE_STRICT and specifier != hostname,
-                "Able to set invalid private DNS strict mode")
+        for dns_hostname in invalid_hostnames:
+            ping_result = self._test_invalid_private_dns(
+                self.get_wifi_network(False),
+                cconst.PRIVATE_DNS_MODE_STRICT,
+                dns_hostname)
+            asserts.assert_false(ping_result, "Ping success with invalid DNS.")
+
diff --git a/acts_tests/tests/google/tel/live/TelLiveRilImsKpiTest.py b/acts_tests/tests/google/tel/live/TelLiveRilImsKpiTest.py
new file mode 100644
index 0000000..20a93bb
--- /dev/null
+++ b/acts_tests/tests/google/tel/live/TelLiveRilImsKpiTest.py
@@ -0,0 +1,1354 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2021 - Google
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import time
+from datetime import datetime
+
+from acts.test_decorators import test_tracker_info
+from acts_contrib.test_utils.tel.loggers.telephony_metric_logger import TelephonyMetricLogger
+from acts_contrib.test_utils.tel.TelephonyBaseTest import TelephonyBaseTest
+from acts_contrib.test_utils.tel.tel_data_utils import airplane_mode_test
+from acts_contrib.test_utils.tel.tel_data_utils import reboot_test
+from acts_contrib.test_utils.tel.tel_defines import WFC_MODE_CELLULAR_PREFERRED
+from acts_contrib.test_utils.tel.tel_defines import WFC_MODE_WIFI_PREFERRED
+from acts_contrib.test_utils.tel.tel_defines import MAX_WAIT_TIME_WIFI_CONNECTION
+from acts_contrib.test_utils.tel.tel_logging_utils import start_pixellogger_always_on_logging
+from acts_contrib.test_utils.tel.tel_subscription_utils import get_slot_index_from_voice_sub_id
+from acts_contrib.test_utils.tel.tel_subscription_utils import get_all_sub_id
+from acts_contrib.test_utils.tel.tel_voice_utils import is_phone_in_call_volte
+from acts_contrib.test_utils.tel.tel_voice_utils import is_phone_in_call_iwlan
+from acts_contrib.test_utils.tel.tel_voice_utils import phone_setup_iwlan
+from acts_contrib.test_utils.tel.tel_voice_utils import phone_setup_iwlan_for_subscription
+from acts_contrib.test_utils.tel.tel_voice_utils import phone_setup_volte_for_subscription
+from acts_contrib.test_utils.tel.tel_voice_utils import phone_idle_volte
+from acts_contrib.test_utils.tel.tel_voice_utils import phone_idle_iwlan
+from acts_contrib.test_utils.tel.tel_voice_utils import two_phone_call_short_seq
+from acts_contrib.test_utils.tel.tel_parse_utils import print_nested_dict
+from acts_contrib.test_utils.tel.tel_parse_utils import parse_ims_reg
+from acts_contrib.test_utils.tel.tel_parse_utils import ON_IMS_MM_TEL_CONNECTED_4G_SLOT0
+from acts_contrib.test_utils.tel.tel_parse_utils import ON_IMS_MM_TEL_CONNECTED_4G_SLOT1
+from acts_contrib.test_utils.tel.tel_parse_utils import ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0
+from acts_contrib.test_utils.tel.tel_parse_utils import ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1
+from acts_contrib.test_utils.tel.tel_test_utils import hangup_call
+from acts_contrib.test_utils.tel.tel_test_utils import verify_internet_connection
+from acts_contrib.test_utils.tel.tel_test_utils import check_is_wifi_connected
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
+from acts_contrib.test_utils.tel.tel_test_utils import set_wfc_mode
+from acts_contrib.test_utils.tel.tel_test_utils import wait_for_network_service
+from acts_contrib.test_utils.tel.tel_test_utils import wait_for_log
+from acts.utils import get_current_epoch_time
+
+SETUP_PHONE_FAIL = 'SETUP_PHONE_FAIL'
+VERIFY_NETWORK_FAIL = 'VERIFY_NETWORK_FAIL'
+VERIFY_INTERNET_FAIL = 'VERIFY_INTERNET_FAIL'
+TOGGLE_OFF_APM_FAIL = 'TOGGLE_OFF_APM_FAIL'
+
+CALCULATE_EVERY_N_CYCLES = 10
+
+def test_result(result_list, cycle, min_fail=0, failrate=0):
+    failure_count = len(list(filter(lambda x: (x != True), result_list)))
+    if failure_count >= min_fail:
+        if failure_count >= cycle * failrate:
+            return False
+    return True
+
+def wait_for_wifi_disconnected(ad, wifi_ssid):
+    """Wait until Wifi is disconnected.
+
+    Args:
+        ad: Android object
+        wifi_ssid: to specify the Wifi AP which should be disconnected.
+
+    Returns:
+        True if Wifi is disconnected before time-out. Otherwise False.
+    """
+    wait_time = 0
+    while wait_time < MAX_WAIT_TIME_WIFI_CONNECTION:
+        if check_is_wifi_connected(ad.log, ad, wifi_ssid):
+            ad.droid.wifiToggleState(False)
+            time.sleep(3)
+            wait_time = wait_time + 3
+        else:
+            ad.log.info('Wifi is disconnected.')
+            return True
+
+    if check_is_wifi_connected(ad.log, ad, wifi_ssid):
+        ad.log.error('Wifi still is connected to %s.', wifi_ssid)
+        return False
+    else:
+        ad.log.info('Wifi is disconnected.')
+        return True
+
+class TelLiveRilImsKpiTest(TelephonyBaseTest):
+    def setup_class(self):
+        TelephonyBaseTest.setup_class(self)
+        start_pixellogger_always_on_logging(self.android_devices[0])
+        self.tel_logger = TelephonyMetricLogger.for_test_case()
+        self.user_params["telephony_auto_rerun"] = 0
+        self.reboot_4g_test_cycle = self.user_params.get(
+            'reboot_4g_test_cycle', 1)
+        self.reboot_iwlan_test_cycle = self.user_params.get(
+            'reboot_iwlan_test_cycle', 1)
+        self.cycle_apm_4g_test_cycle = self.user_params.get(
+            'cycle_apm_4g_test_cycle', 1)
+        self.cycle_wifi_in_apm_mode_test_cycle = self.user_params.get(
+            'cycle_wifi_in_apm_mode_test_cycle', 1)
+        self.ims_handover_4g_to_iwlan_with_voice_call_wfc_wifi_preferred_test_cycle = self.user_params.get(
+            'ims_handover_4g_to_iwlan_with_voice_call_wfc_wifi_preferred_test_cycle', 1)
+        self.ims_handover_4g_to_iwlan_wfc_wifi_preferred_test_cycle = self.user_params.get(
+            'ims_handover_4g_to_iwlan_wfc_wifi_preferred_test_cycle', 1)
+        self.ims_handover_iwlan_to_4g_wfc_wifi_preferred_test_cycle = self.user_params.get(
+            'ims_handover_iwlan_to_4g_wfc_wifi_preferred_test_cycle', 1)
+        self.ims_handover_iwlan_to_4g_with_voice_call_wfc_wifi_preferred_test_cycle = self.user_params.get(
+            'ims_handover_iwlan_to_4g_with_voice_call_wfc_wifi_preferred_test_cycle', 1)
+        self.ims_handover_iwlan_to_4g_wfc_cellular_preferred_test_cycle = self.user_params.get(
+            'ims_handover_iwlan_to_4g_wfc_cellular_preferred_test_cycle', 1)
+        self.ims_handover_iwlan_to_4g_with_voice_call_wfc_cellular_preferred_test_cycle = self.user_params.get(
+            'ims_handover_iwlan_to_4g_with_voice_call_wfc_cellular_preferred_test_cycle', 1)
+
+    def teardown_test(self):
+        for ad in self.android_devices:
+            toggle_airplane_mode(self.log, ad, False)
+
+    @test_tracker_info(uuid="d6a59a3c-2bbc-4ed3-a41e-4492b4ab8a50")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_reboot_4g(self):
+        """Reboot UE and measure bootup IMS registration time on LTE.
+
+        Test steps:
+            1. Enable VoLTE at all slots and ensure IMS is registered over LTE
+                cellular network at all slots.
+            2. Reboot UE.
+            3. Parse logcat to calculate IMS registration time on LTE after
+                bootup.
+        """
+        ad = self.android_devices[0]
+        cycle = self.reboot_4g_test_cycle
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        if getattr(ad, 'dsds', False):
+            the_other_slot = 1 - voice_slot
+        else:
+            the_other_slot = None
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '==================> Reboot on LTE %s/%s <==================',
+                attempt+1,
+                cycle)
+
+            sub_id_list = get_all_sub_id(ad)
+            for sub_id in sub_id_list:
+                if not phone_setup_volte_for_subscription(self.log, ad, sub_id):
+                    result.append(SETUP_PHONE_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_network_service(self.log, ad):
+                    result.append(VERIFY_NETWORK_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                begin_time = datetime.now()
+                if reboot_test(self.log, ad):
+                    result.append(True)
+                else:
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS bootup registration at slot %s '
+                    '======',
+                    voice_slot)
+                ad.log.info(result)
+
+                for slot in [voice_slot, the_other_slot]:
+                    if slot is None:
+                        continue
+
+                    ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                        ad, search_intervals, '4g', 'reboot', slot=slot)
+                    ad.log.info(
+                        '====== IMS bootup registration at slot %s ======', slot)
+                    for msg in ims_reg:
+                        print_nested_dict(ad, msg)
+
+                    ad.log.info(
+                        '====== Attempt of parsing fail at slot %s ======' % slot)
+                    for msg in parsing_fail:
+                        ad.log.info(msg)
+
+                    ad.log.warning('====== Summary ======')
+                    ad.log.warning(
+                        '%s/%s cycles failed.',
+                        (len(result) - result.count(True)),
+                        len(result))
+                    for attempt, value in enumerate(result):
+                        if value is not True:
+                            ad.log.warning('Cycle %s: %s', attempt+1, value)
+                    try:
+                        fail_rate = (
+                            len(result) - result.count(True))/len(result)
+                        ad.log.info(
+                            'Fail rate of IMS bootup registration at slot %s: %s',
+                            slot,
+                            fail_rate)
+                    except Exception as e:
+                        ad.log.error(
+                            'Fail rate of IMS bootup registration at slot %s: '
+                            'ERROR (%s)',
+                            slot,
+                            e)
+
+                    ad.log.info(
+                        'Number of trials with valid parsed logs: %s',
+                        len(ims_reg))
+                    ad.log.info(
+                        'Average IMS bootup registration time at slot %s: %s',
+                        slot,
+                        avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="c97dd2f2-9e8a-43d4-9352-b53abe5ac6a4")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_reboot_iwlan(self):
+        """Reboot UE and measure bootup IMS registration time over iwlan.
+
+        Test steps:
+            1. Enable VoLTE at all slots; enable WFC and set WFC mode to
+                Wi-Fi-preferred mode; connect Wi-Fi and ensure IMS is registered
+                at all slots over iwlan.
+            2. Reboot UE.
+            3. Parse logcat to calculate IMS registration time over iwlan after
+                bootup.
+        """
+        ad = self.android_devices[0]
+        cycle = self.reboot_iwlan_test_cycle
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        if getattr(ad, 'dsds', False):
+            the_other_slot = 1 - voice_slot
+        else:
+            the_other_slot = None
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '==================> Reboot on iwlan %s/%s <==================',
+                attempt+1,
+                cycle)
+
+            sub_id_list = get_all_sub_id(ad)
+            for sub_id in sub_id_list:
+                if not phone_setup_iwlan_for_subscription(
+                    self.log,
+                    ad,
+                    sub_id,
+                    False,
+                    WFC_MODE_WIFI_PREFERRED,
+                    self.wifi_network_ssid,
+                    self.wifi_network_pass):
+
+                    result.append(SETUP_PHONE_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+                    wait_for_wifi_disconnected(ad, self.wifi_network_ssid)
+
+            if _continue:
+                if not verify_internet_connection(self.log, ad):
+                    result.append(VERIFY_INTERNET_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                begin_time = datetime.now()
+                if reboot_test(self.log, ad, wifi_ssid=self.wifi_network_ssid):
+                    result.append(True)
+                else:
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS bootup registration at slot %s '
+                    '======',
+                    voice_slot)
+                ad.log.info(result)
+
+                for slot in [voice_slot, the_other_slot]:
+                    if slot is None:
+                        continue
+
+                    ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                        ad, search_intervals, 'iwlan', 'reboot', slot=slot)
+                    ad.log.info(
+                        '====== IMS bootup registration at slot %s ======', slot)
+                    for msg in ims_reg:
+                        print_nested_dict(ad, msg)
+
+                    ad.log.info(
+                        '====== Attempt of parsing fail at slot %s ======' % slot)
+                    for msg in parsing_fail:
+                        ad.log.info(msg)
+
+                    ad.log.warning('====== Summary ======')
+                    ad.log.warning(
+                        '%s/%s cycles failed.',
+                        (len(result) - result.count(True)),
+                        len(result))
+                    for attempt, value in enumerate(result):
+                        if value is not True:
+                            ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                    try:
+                        fail_rate = (
+                            len(result) - result.count(True))/len(result)
+                        ad.log.info(
+                            'Fail rate of IMS bootup registration at slot %s: %s',
+                            slot,
+                            fail_rate)
+                    except Exception as e:
+                        ad.log.error(
+                            'Fail rate of IMS bootup registration at slot %s: '
+                            'ERROR (%s)',
+                            slot,
+                            e)
+
+                    ad.log.info(
+                        'Number of trials with valid parsed logs: %s',
+                        len(ims_reg))
+                    ad.log.info(
+                        'Average IMS bootup registration time at slot %s: %s',
+                        slot, avg_ims_reg_duration)
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="45ed4572-7de9-4e1b-b2ec-58dea722fa3e")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_cycle_airplane_mode_4g(self):
+        """Cycle airplane mode and measure IMS registration time on LTE
+
+        Test steps:
+            1. Enable VoLTE at all slots and ensure IMS is registered on LTE at
+                all slots.
+            2. Cycle airplane mode.
+            3. Parse logcat to calculate IMS registration time right after
+                recovery of cellular service.
+        """
+        ad = self.android_devices[0]
+        cycle = self.cycle_apm_4g_test_cycle
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        if getattr(ad, 'dsds', False):
+            the_other_slot = 1 - voice_slot
+        else:
+            the_other_slot = None
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '============> Cycle airplane mode on LTE %s/%s <============',
+                attempt+1,
+                cycle)
+
+            sub_id_list = get_all_sub_id(ad)
+            for sub_id in sub_id_list:
+                if not phone_setup_volte_for_subscription(self.log, ad, sub_id):
+                    result.append(SETUP_PHONE_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_network_service(self.log, ad):
+                    result.append(VERIFY_NETWORK_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                begin_time = datetime.now()
+                if airplane_mode_test(self.log, ad):
+                    result.append(True)
+                else:
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS registration at slot %s ======',
+                    voice_slot)
+                ad.log.info(result)
+
+                for slot in [voice_slot, the_other_slot]:
+                    if slot is None:
+                        continue
+
+                    ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                        ad, search_intervals, '4g', 'apm', slot=slot)
+                    ad.log.info(
+                        '====== IMS registration at slot %s ======', slot)
+                    for msg in ims_reg:
+                        print_nested_dict(ad, msg)
+
+                    ad.log.info(
+                        '====== Attempt of parsing fail at slot %s ======' % slot)
+                    for msg in parsing_fail:
+                        ad.log.info(msg)
+
+                    ad.log.warning('====== Summary ======')
+                    ad.log.warning('%s/%s cycles failed.', (len(result) - result.count(True)), len(result))
+                    for attempt, value in enumerate(result):
+                        if value is not True:
+                            ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                    try:
+                        fail_rate = (
+                            len(result) - result.count(True))/len(result)
+                        ad.log.info(
+                            'Fail rate of IMS registration at slot %s: %s',
+                            slot,
+                            fail_rate)
+                    except Exception as e:
+                        ad.log.error(
+                            'Fail rate of IMS registration at slot %s: '
+                            'ERROR (%s)',
+                            slot,
+                            e)
+
+                    ad.log.info(
+                        'Number of trials with valid parsed logs: %s',
+                        len(ims_reg))
+                    ad.log.info(
+                        'Average IMS registration time at slot %s: %s',
+                        slot, avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="915c9403-8bbc-45c7-be53-8b0de4191716")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_cycle_wifi_in_apm_mode(self):
+        """Cycle Wi-Fi in airplane mode and measure IMS registration time over
+            iwlan.
+
+        Test steps:
+            1. Enable VoLTE; enable WFC and set WFC mode to Wi-Fi-preferred mode;
+                turn on airplane mode and connect Wi-Fi to ensure IMS is
+                registered over iwlan.
+            2. Cycle Wi-Fi.
+            3. Parse logcat to calculate IMS registration time right after
+                recovery of Wi-Fi connection in airplane mode.
+        """
+        ad = self.android_devices[0]
+        cycle = self.cycle_wifi_in_apm_mode_test_cycle
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '============> Cycle WiFi in airplane mode %s/%s <============',
+                attempt+1,
+                cycle)
+
+            begin_time = datetime.now()
+
+            if not wait_for_wifi_disconnected(ad, self.wifi_network_ssid):
+                result.append(False)
+                self._take_bug_report(
+                    self.test_name, begin_time=get_current_epoch_time())
+                _continue = False
+                if not test_result(result, cycle, 10, 0.1):
+                    exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not phone_setup_iwlan(
+                    self.log,
+                    ad,
+                    True,
+                    WFC_MODE_WIFI_PREFERRED,
+                    self.wifi_network_ssid,
+                    self.wifi_network_pass):
+
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not verify_internet_connection(self.log, ad):
+                    result.append(VERIFY_INTERNET_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_wifi_disconnected(
+                    ad, self.wifi_network_ssid):
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                result.append(True)
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS registration at slot %s ======',
+                    voice_slot)
+                ad.log.info(result)
+
+                ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                    ad, search_intervals, 'iwlan', 'apm')
+                ad.log.info(
+                    '====== IMS registration at slot %s ======', voice_slot)
+                for msg in ims_reg:
+                    ad.log.info(msg)
+
+                ad.log.info(
+                    '====== Attempt of parsing fail at slot %s ======' % voice_slot)
+                for msg in parsing_fail:
+                    ad.log.info(msg)
+
+                ad.log.warning('====== Summary ======')
+                ad.log.warning(
+                    '%s/%s cycles failed.',
+                    (len(result) - result.count(True)),
+                    len(result))
+                for attempt, value in enumerate(result):
+                    if value is not True:
+                        ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                try:
+                    fail_rate = (len(result) - result.count(True))/len(result)
+                    ad.log.info(
+                        'Fail rate of IMS registration at slot %s: %s',
+                        voice_slot,
+                        fail_rate)
+                except Exception as e:
+                    ad.log.error(
+                        'Fail rate of IMS registration at slot %s: ERROR (%s)',
+                        voice_slot,
+                        e)
+
+                ad.log.info(
+                    'Number of trials with valid parsed logs: %s', len(ims_reg))
+                ad.log.info(
+                    'Average IMS registration time at slot %s: %s',
+                    voice_slot, avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+        toggle_airplane_mode(self.log, ad, False)
+        return test_result(result, cycle)
+
+    def ims_handover_4g_to_iwlan_wfc_wifi_preferred(self, voice_call=False):
+        """Connect WFC to make IMS registration hand over from LTE to iwlan in
+            Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC and set WFC mode to Wi-Fi-preferred mode.
+            2. Ensure Wi-Fi are disconnected and all cellular services are
+                available.
+            3. (Optional) Make a VoLTE call and keep the call active.
+            4. Connect Wi-Fi. The IMS registration should hand over from LTE
+                to iwlan.
+            5. Parse logcat to calculate the IMS handover time.
+
+        Args:
+            voice_call: True if an active VoLTE call is desired in the background
+                during IMS handover procedure. Otherwise False.
+        """
+        ad = self.android_devices[0]
+        if voice_call:
+            cycle = self.ims_handover_4g_to_iwlan_with_voice_call_wfc_wifi_preferred_test_cycle
+        else:
+            cycle = self.ims_handover_4g_to_iwlan_wfc_wifi_preferred_test_cycle
+
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+
+        if not set_wfc_mode(self.log, ad, WFC_MODE_WIFI_PREFERRED):
+            return False
+
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '======> IMS handover from LTE to iwlan in WFC wifi-preferred '
+                'mode %s/%s <======',
+                attempt+1,
+                cycle)
+
+            begin_time = datetime.now()
+
+            if not wait_for_wifi_disconnected(ad, self.wifi_network_ssid):
+                result.append(False)
+                self._take_bug_report(
+                    self.test_name, begin_time=get_current_epoch_time())
+                _continue = False
+                if not test_result(result, cycle, 10, 0.1):
+                    exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_network_service(
+                    self.log,
+                    ad,
+                    wifi_connected=False,
+                    ims_reg=True):
+
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_call:
+                    ad_mt = self.android_devices[1]
+                    call_params = [(
+                        ad,
+                        ad_mt,
+                        None,
+                        is_phone_in_call_volte,
+                        None)]
+                    call_result = two_phone_call_short_seq(
+                        self.log,
+                        ad,
+                        phone_idle_volte,
+                        is_phone_in_call_volte,
+                        ad_mt,
+                        None,
+                        None,
+                        wait_time_in_call=30,
+                        call_params=call_params)
+                    self.tel_logger.set_result(call_result.result_value)
+                    if not call_result:
+                        self._take_bug_report(
+                            self.test_name, begin_time=get_current_epoch_time())
+                        _continue = False
+                        if not test_result(result, cycle, 10, 0.1):
+                            exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not phone_setup_iwlan(
+                    self.log,
+                    ad,
+                    False,
+                    WFC_MODE_WIFI_PREFERRED,
+                    self.wifi_network_ssid,
+                    self.wifi_network_pass):
+
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from LTE to iwlan.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from LTE to '
+                        'iwlan.')
+
+            if voice_call:
+                hangup_call(self.log, ad)
+
+            if _continue:
+                if not verify_internet_connection(self.log, ad):
+                    result.append(VERIFY_INTERNET_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_wifi_disconnected(
+                    ad, self.wifi_network_ssid):
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from iwlan to LTE.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from iwlan to '
+                        'LTE.')
+
+            if _continue:
+                result.append(True)
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS registration at slot %s ======',
+                    voice_slot)
+                ad.log.info(result)
+
+                ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                    ad, search_intervals, 'iwlan', 'apm')
+                ad.log.info(
+                    '====== IMS registration at slot %s ======', voice_slot)
+                for msg in ims_reg:
+                    ad.log.info(msg)
+
+                ad.log.info(
+                    '====== Attempt of parsing fail at slot %s ======' % voice_slot)
+                for msg in parsing_fail:
+                    ad.log.info(msg)
+
+                ad.log.warning('====== Summary ======')
+                ad.log.warning(
+                    '%s/%s cycles failed.',
+                    (len(result) - result.count(True)),
+                    len(result))
+                for attempt, value in enumerate(result):
+                    if value is not True:
+                        ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                try:
+                    fail_rate = (len(result) - result.count(True))/len(result)
+                    ad.log.info(
+                        'Fail rate of IMS registration at slot %s: %s',
+                        voice_slot,
+                        fail_rate)
+                except Exception as e:
+                    ad.log.error(
+                        'Fail rate of IMS registration at slot %s: ERROR (%s)',
+                        voice_slot,
+                        e)
+
+                ad.log.info(
+                    'Number of trials with valid parsed logs: %s',len(ims_reg))
+                ad.log.info(
+                    'Average IMS registration time at slot %s: %s',
+                    voice_slot, avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="e3d1aaa8-f673-4a2b-adb1-cfa525a4edbd")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_4g_to_iwlan_with_voice_call_wfc_wifi_preferred(self):
+        """Connect WFC to make IMS registration hand over from LTE to iwlan in
+            Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC and set WFC mode to Wi-Fi-preferred mode.
+            2. Ensure Wi-Fi are disconnected and all cellular services are
+                available.
+            3. Make a VoLTE call and keep the call active.
+            4. Connect Wi-Fi. The IMS registration should hand over from LTE
+                to iwlan.
+            5. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_4g_to_iwlan_wfc_wifi_preferred(True)
+
+    @test_tracker_info(uuid="bd86fb46-04bd-4642-923a-747e6c9d4282")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_4g_to_iwlan_wfc_wifi_preferred(self):
+        """Connect WFC to make IMS registration hand over from LTE to iwlan in
+            Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC and set WFC mode to Wi-Fi-preferred mode.
+            2. Ensure Wi-Fi are disconnected and all cellular services are
+                available.
+            3. Connect Wi-Fi. The IMS registration should hand over from LTE
+                to iwlan.
+            4. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_4g_to_iwlan_wfc_wifi_preferred(False)
+
+    def ims_handover_iwlan_to_4g_wfc_wifi_preferred(self, voice_call=False):
+        """Disconnect Wi-Fi to make IMS registration hand over from iwlan to LTE
+            in Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to Wi-Fi-preferred mode, and then
+                connect Wi-Fi to let IMS register over iwlan.
+            2. (Optional) Make a WFC call and keep the call active.
+            3. Disconnect Wi-Fi. The IMS registration should hand over from iwlan
+                to LTE.
+            4. Parse logcat to calculate the IMS handover time.
+
+        Args:
+            voice_call: True if an active WFC call is desired in the background
+                during IMS handover procedure. Otherwise False.
+        """
+        ad = self.android_devices[0]
+        if voice_call:
+            cycle = self.ims_handover_iwlan_to_4g_with_voice_call_wfc_wifi_preferred_test_cycle
+        else:
+            cycle = self.ims_handover_iwlan_to_4g_wfc_wifi_preferred_test_cycle
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+            self.log.info(
+                '======> IMS handover from iwlan to LTE in WFC wifi-preferred '
+                'mode %s/%s <======',
+                attempt+1,
+                cycle)
+
+            begin_time = datetime.now()
+
+            if not phone_setup_iwlan(
+                self.log,
+                ad,
+                False,
+                WFC_MODE_WIFI_PREFERRED,
+                self.wifi_network_ssid,
+                self.wifi_network_pass):
+
+                result.append(False)
+                self._take_bug_report(
+                    self.test_name, begin_time=get_current_epoch_time())
+                _continue = False
+                if not test_result(result, cycle, 10, 0.1):
+                    exit_due_to_high_fail_rate = True
+
+                wait_for_wifi_disconnected(ad, self.wifi_network_ssid)
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from LTE to iwlan.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from LTE to '
+                        'iwlan.')
+
+            if _continue:
+                if not verify_internet_connection(self.log, ad):
+                    result.append(VERIFY_INTERNET_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_call:
+                    ad_mt = self.android_devices[1]
+                    call_params = [(
+                        ad,
+                        ad_mt,
+                        None,
+                        is_phone_in_call_iwlan,
+                        None)]
+                    call_result = two_phone_call_short_seq(
+                        self.log,
+                        ad,
+                        phone_idle_iwlan,
+                        is_phone_in_call_iwlan,
+                        ad_mt,
+                        None,
+                        None,
+                        wait_time_in_call=30,
+                        call_params=call_params)
+                    self.tel_logger.set_result(call_result.result_value)
+                    if not call_result:
+                        self._take_bug_report(
+                            self.test_name, begin_time=get_current_epoch_time())
+                        _continue = False
+                        if not test_result(result, cycle, 10, 0.1):
+                            exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not wait_for_wifi_disconnected(
+                    ad, self.wifi_network_ssid):
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from iwlan to LTE.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from iwlan to '
+                        'LTE.')
+
+            if voice_call:
+                hangup_call(self.log, ad)
+
+            if _continue:
+                if not wait_for_network_service(
+                    self.log,
+                    ad,
+                    wifi_connected=False,
+                    ims_reg=True):
+
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                result.append(True)
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS registration at slot %s ======',
+                    voice_slot)
+                ad.log.info(result)
+
+                ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                    ad, search_intervals, '4g', 'wifi_off')
+                ad.log.info(
+                    '====== IMS registration at slot %s ======', voice_slot)
+                for msg in ims_reg:
+                    ad.log.info(msg)
+
+                ad.log.info(
+                    '====== Attempt of parsing fail at slot %s ======' % voice_slot)
+                for msg in parsing_fail:
+                    ad.log.info(msg)
+
+                ad.log.warning('====== Summary ======')
+                ad.log.warning(
+                    '%s/%s cycles failed.',
+                    (len(result) - result.count(True)),
+                    len(result))
+                for attempt, value in enumerate(result):
+                    if value is not True:
+                        ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                try:
+                    fail_rate = (len(result) - result.count(True))/len(result)
+                    ad.log.info(
+                        'Fail rate of IMS registration at slot %s: %s',
+                        voice_slot,
+                        fail_rate)
+                except Exception as e:
+                    ad.log.error(
+                        'Fail rate of IMS registration at slot %s: ERROR (%s)',
+                        voice_slot,
+                        e)
+
+                ad.log.info(
+                    'Number of trials with valid parsed logs: %s', len(ims_reg))
+                ad.log.info(
+                    'Average IMS registration time at slot %s: %s',
+                    voice_slot, avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="6ce623a6-7ef9-42db-8099-d5c449e70bff")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_iwlan_to_4g_wfc_wifi_preferred(self):
+        """Disconnect Wi-Fi to make IMS registration hand over from iwlan to LTE
+            in Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to Wi-Fi-preferred mode, and then
+                connect Wi-Fi to let IMS register over iwlan.
+            2. Disconnect Wi-Fi. The IMS registration should hand over from iwlan
+                to LTE.
+            3. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_iwlan_to_4g_wfc_wifi_preferred(False)
+
+    @test_tracker_info(uuid="b965ab09-d8b1-423f-bb98-2cdd43babbe3")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_iwlan_to_4g_with_voice_call_wfc_wifi_preferred(self):
+        """Disconnect Wi-Fi to make IMS registration hand over from iwlan to LTE
+            in Wi-Fi-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to Wi-Fi-preferred mode, and then
+                connect Wi-Fi to let IMS register over iwlan.
+            2. Make a WFC call and keep the call active.
+            3. Disconnect Wi-Fi. The IMS registration should hand over from iwlan
+                to LTE.
+            4. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_iwlan_to_4g_wfc_wifi_preferred(True)
+
+    def ims_handover_iwlan_to_4g_wfc_cellular_preferred(self, voice_call=False):
+        """Turn off airplane mode to make IMS registration hand over from iwlan to LTE
+            in WFC cellular-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to cellular-preferred mode, turn on
+                airplane mode and then connect Wi-Fi to let IMS register over
+                iwlan.
+            2. (Optional) Make a WFC call and keep the call active.
+            3. Turn off airplane mode. The IMS registration should hand over
+                from iwlan to LTE.
+            4. Parse logcat to calculate the IMS handover time.
+
+        Args:
+            voice_call: True if an active WFC call is desired in the background
+                during IMS handover procedure. Otherwise False.
+        """
+        ad = self.android_devices[0]
+        if voice_call:
+            cycle = self.ims_handover_iwlan_to_4g_with_voice_call_wfc_cellular_preferred_test_cycle
+        else:
+            cycle = self.ims_handover_iwlan_to_4g_wfc_cellular_preferred_test_cycle
+
+        voice_slot = get_slot_index_from_voice_sub_id(ad)
+
+        result = []
+        search_intervals = []
+        exit_due_to_high_fail_rate = False
+        for attempt in range(cycle):
+            _continue = True
+
+            self.log.info(
+                '======> IMS handover from iwlan to LTE in WFC '
+                'cellular-preferred mode %s/%s <======',
+                attempt+1,
+                cycle)
+
+            begin_time = datetime.now()
+
+            if not phone_setup_iwlan(
+                self.log,
+                ad,
+                True,
+                WFC_MODE_CELLULAR_PREFERRED,
+                self.wifi_network_ssid,
+                self.wifi_network_pass):
+
+                result.append(False)
+                self._take_bug_report(
+                    self.test_name, begin_time=get_current_epoch_time())
+                _continue = False
+                if not test_result(result, cycle, 10, 0.1):
+                    exit_due_to_high_fail_rate = True
+
+                toggle_airplane_mode(self.log, ad, False)
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_IWLAN_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from LTE to iwlan.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from LTE to '
+                        'iwlan.')
+
+            if _continue:
+                if not verify_internet_connection(self.log, ad):
+                    result.append(VERIFY_INTERNET_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_call:
+                    ad_mt = self.android_devices[1]
+                    call_params = [(
+                        ad,
+                        ad_mt,
+                        None,
+                        is_phone_in_call_iwlan,
+                        None)]
+                    call_result = two_phone_call_short_seq(
+                        self.log,
+                        ad,
+                        phone_idle_iwlan,
+                        is_phone_in_call_iwlan,
+                        ad_mt,
+                        None,
+                        None,
+                        wait_time_in_call=30,
+                        call_params=call_params)
+                    self.tel_logger.set_result(call_result.result_value)
+                    if not call_result:
+                        self._take_bug_report(
+                            self.test_name, begin_time=get_current_epoch_time())
+                        _continue = False
+                        if not test_result(result, cycle, 10, 0.1):
+                            exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if not toggle_airplane_mode(self.log, ad, False):
+                    result.append(TOGGLE_OFF_APM_FAIL)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                if voice_slot == 0:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT0
+                else:
+                    ims_pattern = ON_IMS_MM_TEL_CONNECTED_4G_SLOT1
+
+                if wait_for_log(ad, ims_pattern, begin_time=begin_time):
+                    ad.log.info(
+                        'IMS registration is handed over from iwlan to LTE.')
+                else:
+                    ad.log.error(
+                        'IMS registration is NOT yet handed over from iwlan to '
+                        'LTE.')
+
+            if voice_call:
+                hangup_call(self.log, ad)
+
+            if _continue:
+                if not wait_for_network_service(
+                    self.log,
+                    ad,
+                    wifi_connected=True,
+                    wifi_ssid=self.wifi_network_ssid,
+                    ims_reg=True):
+
+                    result.append(False)
+                    self._take_bug_report(
+                        self.test_name, begin_time=get_current_epoch_time())
+                    _continue = False
+                    if not test_result(result, cycle, 10, 0.1):
+                        exit_due_to_high_fail_rate = True
+
+            if _continue:
+                result.append(True)
+                end_time = datetime.now()
+                search_intervals.append([begin_time, end_time])
+
+            if (attempt+1) % CALCULATE_EVERY_N_CYCLES == 0 or (
+                attempt == cycle - 1) or exit_due_to_high_fail_rate:
+
+                ad.log.info(
+                    '====== Test result of IMS registration at slot %s ======',
+                    voice_slot)
+                ad.log.info(result)
+
+                ims_reg, parsing_fail, avg_ims_reg_duration = parse_ims_reg(
+                    ad, search_intervals, '4g', 'apm')
+                ad.log.info(
+                    '====== IMS registration at slot %s ======', voice_slot)
+                for msg in ims_reg:
+                    ad.log.info(msg)
+
+                ad.log.info(
+                    '====== Attempt of parsing fail at slot %s ======' % voice_slot)
+                for msg in parsing_fail:
+                    ad.log.info(msg)
+
+                ad.log.warning('====== Summary ======')
+                ad.log.warning(
+                    '%s/%s cycles failed.',
+                    (len(result) - result.count(True)),
+                    len(result))
+                for attempt, value in enumerate(result):
+                    if value is not True:
+                        ad.log.warning('Cycle %s: %s', attempt+1, value)
+
+                try:
+                    fail_rate = (len(result) - result.count(True))/len(result)
+                    ad.log.info(
+                        'Fail rate of IMS registration at slot %s: %s',
+                        voice_slot,
+                        fail_rate)
+                except Exception as e:
+                    ad.log.error(
+                        'Fail rate of IMS registration at slot %s: ERROR (%s)',
+                        voice_slot,
+                        e)
+
+                ad.log.info(
+                    'Number of trials with valid parsed logs: %s', len(ims_reg))
+                ad.log.info(
+                    'Average IMS registration time at slot %s: %s',
+                    voice_slot, avg_ims_reg_duration)
+
+            if exit_due_to_high_fail_rate:
+                break
+
+        return test_result(result, cycle)
+
+    @test_tracker_info(uuid="ce69fac3-931b-4177-82ea-dbae50b2b310")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_iwlan_to_4g_wfc_cellular_preferred(self):
+        """Turn off airplane mode to make IMS registration hand over from iwlan to LTE
+            in WFC cellular-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to cellular-preferred mode, turn on
+                airplane mode and then connect Wi-Fi to let IMS register over
+                iwlan.
+            2. Turn off airplane mode. The IMS registration should hand over
+                from iwlan to LTE.
+            3. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_iwlan_to_4g_wfc_cellular_preferred(False)
+
+    @test_tracker_info(uuid="0ac7d43e-34e6-4ea3-92f4-e413e90a8bc1")
+    @TelephonyBaseTest.tel_test_wrap
+    def test_ims_handover_iwlan_to_4g_with_voice_call_wfc_cellular_preferred(self):
+        """Turn off airplane mode to make IMS registration hand over from iwlan to LTE
+            in WFC cellular-preferred mode. Measure IMS handover time.
+
+        Test steps:
+            1. Enable WFC, set WFC mode to cellular-preferred mode, turn on
+                airplane mode and then connect Wi-Fi to let IMS register over
+                iwlan.
+            2. Make a WFC call and keep the call active.
+            3. Turn off airplane mode. The IMS registration should hand over
+                from iwlan to LTE.
+            4. Parse logcat to calculate the IMS handover time.
+        """
+        return self.ims_handover_iwlan_to_4g_wfc_cellular_preferred(True)
\ No newline at end of file