Merge "Reduce time needed to run test." into main
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 671b5c5..32286e1 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -33,6 +34,8 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -68,6 +71,8 @@
 
     @Before
     public void setup() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
         MockitoAnnotations.initMocks(this);
         mBluetoothFinderManager = new BluetoothFinderManagerSpy();
     }
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index b71890e..f397b37 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -18,6 +18,22 @@
     default_team: "trendy_team_fwk_core_networking",
 }
 
+install_symlink {
+    name: "platform_ethtool_symlink",
+    symlink_target: "/apex/com.android.tethering/bin/ethtool",
+    // installed_location is relative to /system because that's the default partition for soong
+    // modules, unless we add something like `system_ext_specific: true` like in hwservicemanager.
+    installed_location: "bin/ethtool",
+}
+
+phony {
+    name: "mainline_tethering_platform_components",
+    required: [
+        "netbpfload",
+        "platform_ethtool_symlink",
+    ],
+}
+
 cc_binary {
     name: "netbpfload",
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index ce2c2c1..dbececf 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -78,6 +78,7 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PollingCheck
 import com.android.compatibility.common.util.PropertyUtil
+import com.android.compatibility.common.util.SystemUtil
 import com.android.modules.utils.build.SdkLevel.isAtLeastU
 import com.android.net.module.util.DnsPacket
 import com.android.net.module.util.HexDump
@@ -2106,6 +2107,89 @@
         }
     }
 
+    @Test
+    fun testServiceTypeClientRemovedAfterSocketDestroyed() {
+        val si = makeTestServiceInfo(testNetwork1.network)
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+        // Register multiple discovery requests.
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        val discoveryRecord3 = NsdDiscoveryRecord()
+        nsdManager.discoverServices("_test1._tcp", NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, { it.run() }, discoveryRecord1)
+        nsdManager.discoverServices("_test2._tcp", NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, { it.run() }, discoveryRecord2)
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
+
+        tryTest {
+            discoveryRecord1.expectCallback<DiscoveryStarted>()
+            discoveryRecord2.expectCallback<DiscoveryStarted>()
+            discoveryRecord3.expectCallback<DiscoveryStarted>()
+            val foundInfo = discoveryRecord3.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertEquals(testNetwork1.network, foundInfo.network)
+            // Verify that associated ServiceTypeClients has been created for testNetwork1.
+            assertTrue("No serviceTypeClients for testNetwork1.",
+                    hasServiceTypeClientsForNetwork(
+                            getServiceTypeClients(), testNetwork1.network))
+
+            // Disconnect testNetwork1
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1.close(cm)
+            }
+
+            // Verify that no ServiceTypeClients for testNetwork1.
+            discoveryRecord3.expectCallback<ServiceLost>()
+            assertFalse("Still has serviceTypeClients for testNetwork1.",
+                    hasServiceTypeClientsForNetwork(
+                            getServiceTypeClients(), testNetwork1.network))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            nsdManager.stopServiceDiscovery(discoveryRecord3)
+            discoveryRecord1.expectCallback<DiscoveryStopped>()
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+            discoveryRecord3.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
+        for (client in clients) {
+            val netid = client.substring(
+                    client.indexOf("network=") + "network=".length,
+                    client.indexOf("interfaceIndex=") - 1)
+            if (netid == network.toString()) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Get ServiceTypeClient logs from the system dump servicediscovery section.
+     *
+     * The sample output:
+     *     ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+     *         SocketKey{ network=116 interfaceIndex=68 } with 1 listeners.
+     *     ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+     *         SocketKey{ network=115 interfaceIndex=67 } with 1 listeners.
+     */
+    private fun getServiceTypeClients(): List<String> {
+        return SystemUtil.runShellCommand(
+                InstrumentationRegistry.getInstrumentation(), "dumpsys servicediscovery")
+                .split("\n").mapNotNull { line ->
+                    line.indexOf("ServiceTypeClient:").let { idx ->
+                        if (idx == -1) null
+                        else line.substring(idx)
+                    }
+                }
+    }
+
     private fun buildConflictingAnnouncement(): ByteBuffer {
         /*
         Generated with:
@@ -2270,4 +2354,4 @@
     // No duplicate addresses in the actual address list
     assertEquals(actual.toSet().size, actual.size)
     assertEquals(expected.toSet(), actual.toSet())
-}
\ No newline at end of file
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 20d457f..a5d2f4a 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -58,14 +58,11 @@
 filegroup {
     name: "non-connectivity-module-test",
     srcs: [
-        "java/android/net/Ikev2VpnProfileTest.java",
         "java/android/net/IpMemoryStoreTest.java",
         "java/android/net/TelephonyNetworkSpecifierTest.java",
-        "java/android/net/VpnManagerTest.java",
         "java/android/net/ipmemorystore/*.java",
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
-        "java/com/android/internal/net/VpnProfileTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MetricsTestUtil.java",
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
deleted file mode 100644
index e12e961..0000000
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * 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.
- */
-
-package android.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.net.ipsec.ike.IkeKeyIdIdentification;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-import android.test.mock.MockContext;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
-import com.android.net.module.util.ProxyUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.security.auth.x500.X500Principal;
-
-/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class Ikev2VpnProfileTest {
-    private static final String SERVER_ADDR_STRING = "1.2.3.4";
-    private static final String IDENTITY_STRING = "Identity";
-    private static final String USERNAME_STRING = "username";
-    private static final String PASSWORD_STRING = "pa55w0rd";
-    private static final String EXCL_LIST = "exclList";
-    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
-    private static final int TEST_MTU = 1300;
-
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    private final MockContext mMockContext =
-            new MockContext() {
-                @Override
-                public String getOpPackageName() {
-                    return "fooPackage";
-                }
-            };
-    private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
-            SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
-
-    private X509Certificate mUserCert;
-    private X509Certificate mServerRootCa;
-    private PrivateKey mPrivateKey;
-
-    @Before
-    public void setUp() throws Exception {
-        mServerRootCa = generateRandomCertAndKeyPair().cert;
-
-        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
-        mUserCert = userCertKey.cert;
-        mPrivateKey = userCertKey.key;
-    }
-
-    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
-
-        builder.setBypassable(true);
-        builder.setProxy(mProxy);
-        builder.setMaxMtu(TEST_MTU);
-        builder.setMetered(true);
-
-        return builder;
-    }
-
-    @Test
-    public void testBuildValidProfileWithOptions() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        // Check non-auth parameters correctly stored
-        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
-        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
-        assertEquals(mProxy, profile.getProxyInfo());
-        assertTrue(profile.isBypassable());
-        assertTrue(profile.isMetered());
-        assertEquals(TEST_MTU, profile.getMaxMtu());
-        assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testBuildUsernamePasswordProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertEquals(USERNAME_STRING, profile.getUsername());
-        assertEquals(PASSWORD_STRING, profile.getPassword());
-        assertEquals(mServerRootCa, profile.getServerRootCaCert());
-
-        assertNull(profile.getPresharedKey());
-        assertNull(profile.getRsaPrivateKey());
-        assertNull(profile.getUserCert());
-    }
-
-    @Test
-    public void testBuildDigitalSignatureProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertEquals(profile.getUserCert(), mUserCert);
-        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
-        assertEquals(profile.getServerRootCaCert(), mServerRootCa);
-
-        assertNull(profile.getPresharedKey());
-        assertNull(profile.getUsername());
-        assertNull(profile.getPassword());
-    }
-
-    @Test
-    public void testBuildPresharedKeyProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
-
-        assertNull(profile.getServerRootCaCert());
-        assertNull(profile.getUsername());
-        assertNull(profile.getPassword());
-        assertNull(profile.getRsaPrivateKey());
-        assertNull(profile.getUserCert());
-    }
-
-    @Test
-    public void testBuildWithAllowedAlgorithmsAead() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-
-        List<String> allowedAlgorithms =
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
-        builder.setAllowedAlgorithms(allowedAlgorithms);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-
-        List<String> allowedAlgorithms =
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.AUTH_AES_XCBC,
-                        IpSecAlgorithm.AUTH_AES_CMAC,
-                        IpSecAlgorithm.CRYPT_AES_CBC,
-                        IpSecAlgorithm.CRYPT_AES_CTR);
-        builder.setAllowedAlgorithms(allowedAlgorithms);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsEmptyList() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.setAllowedAlgorithms(new ArrayList<>());
-            fail("Expected exception due to no valid algorithm set");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsInvalidList() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
-            fail("Expected exception due to missing encryption");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
-            fail("Expected exception due to missing authentication");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
-            fail("Expected exception due to insecure algorithm");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
-            fail("Expected exception due to insecure algorithm");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testBuildNoAuthMethodSet() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.build();
-            fail("Expected exception due to lack of auth method");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-
-    // TODO: Refer to Build.VERSION_CODES.SC_V2 when it's available in AOSP and mainline branch
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
-    @Test
-    public void testBuildExcludeLocalRoutesSet() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-        builder.setLocalRoutesExcluded(true);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-        assertTrue(profile.areLocalRoutesExcluded());
-
-        builder.setBypassable(false);
-        try {
-            builder.build();
-            fail("Expected exception because excludeLocalRoutes should be set only"
-                    + " on the bypassable VPN");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testBuildInvalidMtu() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.setMaxMtu(500);
-            fail("Expected exception due to too-small MTU");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    private void verifyVpnProfileCommon(VpnProfile profile) {
-        assertEquals(SERVER_ADDR_STRING, profile.server);
-        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
-        assertEquals(mProxy, profile.proxy);
-        assertTrue(profile.isBypassable);
-        assertTrue(profile.isMetered);
-        assertEquals(TEST_MTU, profile.maxMtu);
-    }
-
-    @Test
-    public void testPskConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        verifyVpnProfileCommon(profile);
-        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
-
-        // Check nothing else is set
-        assertEquals("", profile.username);
-        assertEquals("", profile.password);
-        assertEquals("", profile.ipsecUserCert);
-        assertEquals("", profile.ipsecCaCert);
-    }
-
-    @Test
-    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        verifyVpnProfileCommon(profile);
-        assertEquals(USERNAME_STRING, profile.username);
-        assertEquals(PASSWORD_STRING, profile.password);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
-        // Check nothing else is set
-        assertEquals("", profile.ipsecUserCert);
-        assertEquals("", profile.ipsecSecret);
-    }
-
-    @Test
-    public void testRsaConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
-                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
-        verifyVpnProfileCommon(profile);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
-        assertEquals(
-                expectedSecret,
-                profile.ipsecSecret);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
-        // Check nothing else is set
-        assertEquals("", profile.username);
-        assertEquals("", profile.password);
-    }
-
-    @Test
-    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.username = USERNAME_STRING;
-        profile.password = PASSWORD_STRING;
-        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
-        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getUsername());
-        assertNull(result.getPassword());
-        assertNull(result.getUserCert());
-        assertNull(result.getRsaPrivateKey());
-        assertNull(result.getServerRootCaCert());
-    }
-
-    @Test
-    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.ipsecSecret = new String(PSK_BYTES);
-        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getPresharedKey());
-        assertNull(result.getUserCert());
-        assertNull(result.getRsaPrivateKey());
-    }
-
-    @Test
-    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.username = USERNAME_STRING;
-        profile.password = PASSWORD_STRING;
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getUsername());
-        assertNull(result.getPassword());
-        assertNull(result.getPresharedKey());
-    }
-
-    @Test
-    public void testPskConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testUsernamePasswordConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testRsaConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception {
-        // Special keyId that contains delimiter character of VpnProfile
-        final byte[] keyId = "foo\0bar".getBytes();
-        final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams(
-                getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)),
-                CHILD_PARAMS);
-        final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
-        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
-
-        assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type);
-
-        // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and
-        // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set.
-        assertEquals("", vpnProfile.server);
-        assertEquals("", vpnProfile.ipsecIdentifier);
-        assertEquals("", vpnProfile.username);
-        assertEquals("", vpnProfile.password);
-        assertEquals("", vpnProfile.ipsecCaCert);
-        assertEquals("", vpnProfile.ipsecSecret);
-        assertEquals("", vpnProfile.ipsecUserCert);
-        assertEquals(0, vpnProfile.getAllowedAlgorithms().size());
-
-        // IkeTunnelConnectionParams should stay the same.
-        assertEquals(tunnelParams, vpnProfile.ikeTunConnParams);
-
-        // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same.
-        final VpnProfile decodedVpnProfile =
-                VpnProfile.decode(vpnProfile.key, vpnProfile.encode());
-        final Ikev2VpnProfile convertedIkev2VpnProfile =
-                Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile);
-        assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile);
-    }
-
-    @Test
-    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        // Config authentication related fields is not required while building with
-        // IkeTunnelConnectionParams.
-        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAutomaticNattKeepaliveTimerEnabled(true);
-        builder.setAutomaticIpVersionSelectionEnabled(true);
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testAutomaticNattAndIpVersionDefaults() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
-        assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
-    }
-
-    @Test
-    public void testEquals() throws Exception {
-        // Verify building without IkeTunnelConnectionParams
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        assertEquals(builder.build(), builder.build());
-
-        // Verify building with IkeTunnelConnectionParams
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        final IkeTunnelConnectionParams tunnelParams2 =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
-                new Ikev2VpnProfile.Builder(tunnelParams2).build());
-    }
-
-    @Test
-    public void testBuildProfileWithNullProxy() throws Exception {
-        final Ikev2VpnProfile ikev2VpnProfile =
-                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
-                        .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
-                        .build();
-
-        // ProxyInfo should be null for the profile without setting ProxyInfo.
-        assertNull(ikev2VpnProfile.getProxyInfo());
-
-        // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
-        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
-        assertNull(vpnProfile.proxy);
-
-        final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
-        assertNull(convertedIkev2VpnProfile.getProxyInfo());
-    }
-
-    private static class CertificateAndKey {
-        public final X509Certificate cert;
-        public final PrivateKey key;
-
-        CertificateAndKey(X509Certificate cert, PrivateKey key) {
-            this.cert = cert;
-            this.key = key;
-        }
-    }
-
-    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
-        final Date validityBeginDate =
-                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
-        final Date validityEndDate =
-                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
-
-        // Generate a keypair
-        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
-        keyPairGenerator.initialize(512);
-        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
-
-        final X500Principal dnName = new X500Principal("CN=test.android.com");
-        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
-        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
-        certGen.setSubjectDN(dnName);
-        certGen.setIssuerDN(dnName);
-        certGen.setNotBefore(validityBeginDate);
-        certGen.setNotAfter(validityEndDate);
-        certGen.setPublicKey(keyPair.getPublic());
-        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
-
-        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
-        return new CertificateAndKey(cert, keyPair.getPrivate());
-    }
-}
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
deleted file mode 100644
index 2ab4e45..0000000
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * 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.
- */
-
-package android.net;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.test.mock.MockContext;
-import android.util.SparseArray;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.MessageUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link VpnManager}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnManagerTest {
-
-    private static final String PKG_NAME = "fooPackage";
-
-    private static final String SESSION_NAME_STRING = "testSession";
-    private static final String SERVER_ADDR_STRING = "1.2.3.4";
-    private static final String IDENTITY_STRING = "Identity";
-    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
-
-    private IVpnManager mMockService;
-    private VpnManager mVpnManager;
-    private final MockContext mMockContext =
-            new MockContext() {
-                @Override
-                public String getOpPackageName() {
-                    return PKG_NAME;
-                }
-            };
-
-    @Before
-    public void setUp() throws Exception {
-        assumeFalse("Skipping test because watches don't support VPN",
-            InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_WATCH));
-        mMockService = mock(IVpnManager.class);
-        mVpnManager = new VpnManager(mMockContext, mMockService);
-    }
-
-    @Test
-    public void testProvisionVpnProfilePreconsented() throws Exception {
-        final PlatformVpnProfile profile = getPlatformVpnProfile();
-        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
-                .thenReturn(true);
-
-        // Expect there to be no intent returned, as consent has already been granted.
-        assertNull(mVpnManager.provisionVpnProfile(profile));
-        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
-    }
-
-    @Test
-    public void testProvisionVpnProfileNeedsConsent() throws Exception {
-        final PlatformVpnProfile profile = getPlatformVpnProfile();
-        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
-                .thenReturn(false);
-
-        // Expect intent to be returned, as consent has not already been granted.
-        final Intent intent = mVpnManager.provisionVpnProfile(profile);
-        assertNotNull(intent);
-
-        final ComponentName expectedComponentName =
-                ComponentName.unflattenFromString(
-                        "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
-        assertEquals(expectedComponentName, intent.getComponent());
-        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
-    }
-
-    @Test
-    public void testDeleteProvisionedVpnProfile() throws Exception {
-        mVpnManager.deleteProvisionedVpnProfile();
-        verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
-    }
-
-    @Test
-    public void testStartProvisionedVpnProfile() throws Exception {
-        mVpnManager.startProvisionedVpnProfile();
-        verify(mMockService).startVpnProfile(eq(PKG_NAME));
-    }
-
-    @Test
-    public void testStopProvisionedVpnProfile() throws Exception {
-        mVpnManager.stopProvisionedVpnProfile();
-        verify(mMockService).stopVpnProfile(eq(PKG_NAME));
-    }
-
-    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
-        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
-                .setBypassable(true)
-                .setMaxMtu(1300)
-                .setMetered(true)
-                .setAuthPsk(PSK_BYTES)
-                .build();
-    }
-
-    @Test
-    public void testVpnTypesEqual() throws Exception {
-        SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
-                new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
-        SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
-                new Class[] { NativeVpnType.class }, new String[]{ "" });
-
-        // TYPE_VPN_NONE = -1 is only defined in VpnManager.
-        assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
-        for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
-            assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
-        }
-    }
-}
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
deleted file mode 100644
index acae7d2..0000000
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * 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.
- */
-
-package com.android.internal.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
-
-import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
-import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
-import static com.android.testutils.ParcelUtils.assertParcelSane;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.net.IpSecAlgorithm;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** Unit tests for {@link VpnProfile}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnProfileTest {
-    private static final String DUMMY_PROFILE_KEY = "Test";
-
-    private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
-    private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
-    private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
-    private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
-    private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
-    private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
-    private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
-
-    @Test
-    public void testDefaults() throws Exception {
-        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
-
-        assertEquals(DUMMY_PROFILE_KEY, p.key);
-        assertEquals("", p.name);
-        assertEquals(VpnProfile.TYPE_PPTP, p.type);
-        assertEquals("", p.server);
-        assertEquals("", p.username);
-        assertEquals("", p.password);
-        assertEquals("", p.dnsServers);
-        assertEquals("", p.searchDomains);
-        assertEquals("", p.routes);
-        assertTrue(p.mppe);
-        assertEquals("", p.l2tpSecret);
-        assertEquals("", p.ipsecIdentifier);
-        assertEquals("", p.ipsecSecret);
-        assertEquals("", p.ipsecUserCert);
-        assertEquals("", p.ipsecCaCert);
-        assertEquals("", p.ipsecServerCert);
-        assertEquals(null, p.proxy);
-        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
-        assertFalse(p.isBypassable);
-        assertFalse(p.isMetered);
-        assertEquals(1360, p.maxMtu);
-        assertFalse(p.areAuthParamsInline);
-        assertFalse(p.isRestrictedToTestNetworks);
-        assertFalse(p.excludeLocalRoutes);
-        assertFalse(p.requiresInternetValidation);
-        assertFalse(p.automaticNattKeepaliveTimerEnabled);
-        assertFalse(p.automaticIpVersionSelectionEnabled);
-    }
-
-    private VpnProfile getSampleIkev2Profile(String key) {
-        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
-                true /* automaticIpVersionSelectionEnabled */);
-
-        p.name = "foo";
-        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
-        p.server = "bar";
-        p.username = "baz";
-        p.password = "qux";
-        p.dnsServers = "8.8.8.8";
-        p.searchDomains = "";
-        p.routes = "0.0.0.0/0";
-        p.mppe = false;
-        p.l2tpSecret = "";
-        p.ipsecIdentifier = "quux";
-        p.ipsecSecret = "quuz";
-        p.ipsecUserCert = "corge";
-        p.ipsecCaCert = "grault";
-        p.ipsecServerCert = "garply";
-        p.proxy = null;
-        p.setAllowedAlgorithms(
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.CRYPT_AES_CBC));
-        p.isBypassable = true;
-        p.isMetered = true;
-        p.maxMtu = 1350;
-        p.areAuthParamsInline = true;
-
-        // Not saved, but also not compared.
-        p.saveLogin = true;
-
-        return p;
-    }
-
-    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
-        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
-                true /* mAutomaticNattKeepaliveTimerEnabled */,
-                true /* automaticIpVersionSelectionEnabled */);
-
-        p.name = "foo";
-        p.server = "bar";
-        p.dnsServers = "8.8.8.8";
-        p.searchDomains = "";
-        p.routes = "0.0.0.0/0";
-        p.mppe = false;
-        p.proxy = null;
-        p.setAllowedAlgorithms(
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.CRYPT_AES_CBC));
-        p.isBypassable = true;
-        p.isMetered = true;
-        p.maxMtu = 1350;
-        p.areAuthParamsInline = true;
-
-        // Not saved, but also not compared.
-        p.saveLogin = true;
-
-        return p;
-    }
-
-    @Test
-    public void testEquals() {
-        assertEquals(
-                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
-
-        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        modified.maxMtu--;
-        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
-    }
-
-    @Test
-    public void testParcelUnparcel() {
-        if (isAtLeastU()) {
-            // automaticNattKeepaliveTimerEnabled, automaticIpVersionSelectionEnabled added in U.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
-        } else if (isAtLeastT()) {
-            // excludeLocalRoutes, requiresPlatformValidation were added in T.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
-        } else {
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
-        }
-    }
-
-    @Test
-    public void testEncodeDecodeWithIkeTunConnParams() {
-        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testEncodeDecode() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testEncodeDecodeTooManyValues() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final byte[] tooManyValues =
-                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
-
-        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
-    }
-
-    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
-        // Sort to ensure when we remove, we can do it from greatest first.
-        Arrays.sort(missingIndices);
-
-        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
-        final List<String> parts =
-                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
-
-        // Remove from back first to ensure indexing is consistent.
-        for (int i = missingIndices.length - 1; i >= 0; i--) {
-            parts.remove(missingIndices[i]);
-        }
-
-        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
-    }
-
-    @Test
-    public void testEncodeDecodeInvalidNumberOfValues() {
-        final String tooFewValues =
-                getEncodedDecodedIkev2ProfileMissingValues(
-                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
-                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
-                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
-                        ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
-                        ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
-                        ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
-                        /* missingIndices */);
-
-        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
-    }
-
-    private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
-        return getEncodedDecodedIkev2ProfileMissingValues(
-                ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
-                ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
-                ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
-                ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
-                ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without isRestrictedToTestNetworks defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.isRestrictedToTestNetworks);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingExcludeLocalRoutes() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without excludeLocalRoutes defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.excludeLocalRoutes);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingRequiresValidation() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without requiresValidation defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.requiresInternetValidation);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.automaticIpVersionSelectionEnabled);
-    }
-
-    @Test
-    public void testEncodeDecodeLoginsNotSaved() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        profile.saveLogin = false;
-
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertNotEquals(profile, decoded);
-
-        // Add the username/password back, everything else must be equal.
-        decoded.username = profile.username;
-        decoded.password = profile.password;
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testClone() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final VpnProfile clone = profile.clone();
-        assertEquals(profile, clone);
-        assertNotSame(profile, clone);
-    }
-}
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
index 25060d1..86f6b69 100644
--- a/thread/apex/ot-daemon.34rc
+++ b/thread/apex/ot-daemon.34rc
@@ -21,5 +21,5 @@
     user thread_network
     group thread_network inet system
     seclabel u:r:ot_daemon:s0
-    socket ot-daemon/thread-wpan.sock stream 0666 thread_network thread_network
+    socket ot-daemon/thread-wpan.sock stream 0660 thread_network thread_network
     override
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index c74c023..440c2c3 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.net.InetAddresses;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Handler;
@@ -33,6 +34,7 @@
 import com.android.server.thread.openthread.INsdPublisher;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
+import java.net.InetAddress;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
@@ -119,6 +121,30 @@
         return serviceInfo;
     }
 
+    @Override
+    public void registerHost(
+            String name, List<String> addresses, INsdStatusReceiver receiver, int listenerId) {
+        postRegistrationJob(
+                () -> {
+                    NsdServiceInfo serviceInfo = buildServiceInfoForHost(name, addresses);
+                    registerInternal(serviceInfo, receiver, listenerId, "host");
+                });
+    }
+
+    private static NsdServiceInfo buildServiceInfoForHost(
+            String name, List<String> addressStrings) {
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+        serviceInfo.setHostname(name);
+        ArrayList<InetAddress> addresses = new ArrayList<>(addressStrings.size());
+        for (String addressString : addressStrings) {
+            addresses.add(InetAddresses.parseNumericAddress(addressString));
+        }
+        serviceInfo.setHostAddresses(addresses);
+
+        return serviceInfo;
+    }
+
     private void registerInternal(
             NsdServiceInfo serviceInfo,
             INsdStatusReceiver receiver,
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 6ba192d..9677ec5 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -31,6 +31,7 @@
         "net-utils-device-common",
         "net-utils-device-common-bpf",
         "testables",
+        "ThreadNetworkTestUtils",
         "truth",
     ],
     libs: [
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
new file mode 100644
index 0000000..9bc92c7
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.net.thread;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
+import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.utils.IntegrationTestUtils.resolveService;
+import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.TapTestNetworkTracker;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
+
+/** Integration test cases for Service Discovery feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ServiceDiscoveryTest {
+    private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
+    private static final int NUM_FTD = 3;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    private HandlerThread mHandlerThread;
+    private ThreadNetworkController mController;
+    private NsdManager mNsdManager;
+    private TapTestNetworkTracker mTestNetworkTracker;
+    private List<FullThreadDevice> mFtds;
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    private static final Correspondence<byte[], byte[]> BYTE_ARRAY_EQUALITY =
+            Correspondence.from(Arrays::equals, "is equivalent to");
+
+    @Before
+    public void setUp() throws Exception {
+        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
+
+        // Run the tests on only devices where the Thread feature is available.
+        assumeNotNull(mController);
+
+        // Run the tests only when the device uses simulated Thread radio.
+        assumeTrue(isSimulatedThreadRadioSupported());
+
+        // BR forms a network.
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+        joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
+
+        mNsdManager = mContext.getSystemService(NsdManager.class);
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+        assertThat(mTestNetworkTracker).isNotNull();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    CompletableFuture<Void> future = new CompletableFuture<>();
+                    mController.setTestNetworkAsUpstream(
+                            mTestNetworkTracker.getInterfaceName(),
+                            directExecutor(),
+                            v -> future.complete(null));
+                    future.get(5, SECONDS);
+                });
+        // Create the FTDs in setUp() so that the FTDs can be safely released in tearDown().
+        // Don't create new FTDs in test cases.
+        mFtds = new ArrayList<>();
+        for (int i = 0; i < NUM_FTD; ++i) {
+            FullThreadDevice ftd = new FullThreadDevice(10 + i /* node ID */);
+            ftd.autoStartSrpClient();
+            mFtds.add(ftd);
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mController == null) {
+            return;
+        }
+        if (!isSimulatedThreadRadioSupported()) {
+            return;
+        }
+        for (FullThreadDevice ftd : mFtds) {
+            // Clear registered SRP hosts and services
+            if (ftd.isSrpHostRegistered()) {
+                ftd.removeSrpHost();
+            }
+            ftd.destroy();
+        }
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
+        }
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    CompletableFuture<Void> setUpstreamFuture = new CompletableFuture<>();
+                    CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+                    mController.setTestNetworkAsUpstream(
+                            null, directExecutor(), v -> setUpstreamFuture.complete(null));
+                    mController.leave(directExecutor(), v -> leaveFuture.complete(null));
+                    setUpstreamFuture.get(5, SECONDS);
+                    leaveFuture.get(5, SECONDS);
+                });
+    }
+
+    @Test
+    public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device 1
+         *  (Cuttlefish)         |
+         *                       +------ Full Thread device 2
+         *                       |
+         *                       +------ Full Thread device 3
+         * </pre>
+         */
+
+        // Creates Full Thread Devices (FTD) and let them join the network.
+        for (FullThreadDevice ftd : mFtds) {
+            ftd.joinNetwork(DEFAULT_DATASET);
+            ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        }
+
+        int randomId = new Random().nextInt(10_000);
+
+        String serviceNamePrefix = "service-" + randomId + "-";
+        String serviceTypePrefix = "_test" + randomId;
+        String hostnamePrefix = "host-" + randomId + "-";
+
+        // For every FTD, let it register an SRP service.
+        for (int i = 0; i < mFtds.size(); ++i) {
+            FullThreadDevice ftd = mFtds.get(i);
+            ftd.setSrpHostname(hostnamePrefix + i);
+            ftd.setSrpHostAddresses(List.of(ftd.getOmrAddress(), ftd.getMlEid()));
+            ftd.addSrpService(
+                    serviceNamePrefix + i,
+                    serviceTypePrefix + i + "._tcp",
+                    List.of("_sub1", "_sub2"),
+                    12345 /* port */,
+                    Map.of("key1", bytes(0x01, 0x02), "key2", bytes(i)));
+        }
+
+        // Check the advertised services are discoverable and resolvable by NsdManager
+        for (int i = 0; i < mFtds.size(); ++i) {
+            NsdServiceInfo discoveredService =
+                    discoverService(mNsdManager, serviceTypePrefix + i + "._tcp");
+            assertThat(discoveredService).isNotNull();
+            NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
+            assertThat(resolvedService.getServiceName()).isEqualTo(serviceNamePrefix + i);
+            assertThat(resolvedService.getServiceType()).isEqualTo(serviceTypePrefix + i + "._tcp");
+            assertThat(resolvedService.getPort()).isEqualTo(12345);
+            assertThat(resolvedService.getAttributes())
+                    .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                    .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(i));
+            assertThat(resolvedService.getHostname()).isEqualTo(hostnamePrefix + i);
+            assertThat(resolvedService.getHostAddresses())
+                    .containsExactly(mFtds.get(i).getOmrAddress());
+        }
+    }
+
+    @Test
+    public void advertisingProxy_srpClientUpdatesService_updatedServiceResolvableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        // Creates a Full Thread Devices (FTD) and let it join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        ftd.setSrpHostname("my-host");
+        ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
+        ftd.addSrpService(
+                "my-service",
+                "_test._tcp",
+                Collections.emptyList() /* subtypes */,
+                12345 /* port */,
+                Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+        // Update the host addresses
+        ftd.setSrpHostAddresses(
+                List.of(
+                        (Inet6Address) parseNumericAddress("2001:db8::1"),
+                        (Inet6Address) parseNumericAddress("2001:db8::2")));
+        // Update the service
+        ftd.updateSrpService(
+                "my-service", "_test._tcp", List.of("_sub3"), 11111, Map.of("key1", bytes(0x04)));
+        waitFor(ftd::isSrpHostRegistered, SERVICE_DISCOVERY_TIMEOUT);
+
+        // Check the advertised service is discoverable and resolvable by NsdManager
+        NsdServiceInfo discoveredService = discoverService(mNsdManager, "_test._tcp");
+        assertThat(discoveredService).isNotNull();
+        NsdServiceInfo resolvedService =
+                resolveServiceUntil(
+                        mNsdManager,
+                        discoveredService,
+                        s -> s.getPort() == 11111 && s.getHostAddresses().size() == 2);
+        assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+        assertThat(resolvedService.getServiceType()).isEqualTo("_test._tcp");
+        assertThat(resolvedService.getPort()).isEqualTo(11111);
+        assertThat(resolvedService.getAttributes())
+                .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                .containsExactly("key1", bytes(0x04));
+        assertThat(resolvedService.getHostname()).isEqualTo("my-host");
+        assertThat(resolvedService.getHostAddresses())
+                .containsExactly(
+                        parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
+    }
+
+    @Test
+    public void advertisingProxy_srpClientUnregistersService_serviceIsNotDiscoverableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        // Creates a Full Thread Devices (FTD) and let it join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        ftd.joinNetwork(DEFAULT_DATASET);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        ftd.setSrpHostname("my-host");
+        ftd.setSrpHostAddresses(
+                List.of(
+                        (Inet6Address) parseNumericAddress("2001:db8::1"),
+                        (Inet6Address) parseNumericAddress("2001:db8::2")));
+        ftd.addSrpService(
+                "my-service",
+                "_test._udp",
+                List.of("_sub1"),
+                12345 /* port */,
+                Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+        // Wait for the service to be discoverable by NsdManager.
+        assertThat(discoverService(mNsdManager, "_test._udp")).isNotNull();
+
+        // Unregister the service.
+        CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+        NsdManager.DiscoveryListener listener =
+                discoverForServiceLost(mNsdManager, "_test._udp", serviceLostFuture);
+        ftd.removeSrpService("my-service", "_test._udp", true /* notifyServer */);
+
+        // Verify the service becomes lost.
+        try {
+            serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            mNsdManager.stopServiceDiscovery(listener);
+        }
+        assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_test._udp"));
+    }
+
+    private static byte[] bytes(int... byteInts) {
+        byte[] bytes = new byte[byteInts.length];
+        for (int i = 0; i < byteInts.length; ++i) {
+            bytes[i] = (byte) byteInts[i];
+        }
+        return bytes;
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 6cb1675..6306a65 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -15,6 +15,7 @@
  */
 package android.net.thread.utils;
 
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -25,15 +26,19 @@
 import android.net.IpPrefix;
 import android.net.thread.ActiveOperationalDataset;
 
+import com.google.errorprone.annotations.FormatMethod;
+
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.net.Inet6Address;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -191,7 +196,7 @@
     public void udpBind(Inet6Address address, int port) {
         udpClose();
         udpOpen();
-        executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port));
+        executeCommand("udp bind %s %d", address.getHostAddress(), port);
     }
 
     /** Returns the message received on the UDP socket. */
@@ -204,6 +209,117 @@
         return matcher.group(4);
     }
 
+    /** Enables the SRP client and run in autostart mode. */
+    public void autoStartSrpClient() {
+        executeCommand("srp client autostart enable");
+    }
+
+    /** Sets the hostname (e.g. "MyHost") for the SRP client. */
+    public void setSrpHostname(String hostname) {
+        executeCommand("srp client host name " + hostname);
+    }
+
+    /** Sets the host addresses for the SRP client. */
+    public void setSrpHostAddresses(List<Inet6Address> addresses) {
+        executeCommand(
+                "srp client host address "
+                        + String.join(
+                                " ",
+                                addresses.stream().map(Inet6Address::getHostAddress).toList()));
+    }
+
+    /** Removes the SRP host */
+    public void removeSrpHost() {
+        executeCommand("srp client host remove 1 1");
+    }
+
+    /**
+     * Adds an SRP service for the SRP client and wait for the registration to complete.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param subtypes the service subtypes like "_sub1"
+     * @param port the port number in range [1, 65535]
+     * @param txtMap the map of TXT names and values
+     * @throws TimeoutException if the service isn't registered within timeout
+     */
+    public void addSrpService(
+            String serviceName,
+            String serviceType,
+            List<String> subtypes,
+            int port,
+            Map<String, byte[]> txtMap)
+            throws TimeoutException {
+        StringBuilder fullServiceType = new StringBuilder(serviceType);
+        for (String subtype : subtypes) {
+            fullServiceType.append(",").append(subtype);
+        }
+        executeCommand(
+                "srp client service add %s %s %d %d %d %s",
+                serviceName,
+                fullServiceType,
+                port,
+                0 /* priority */,
+                0 /* weight */,
+                txtMapToHexString(txtMap));
+        waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
+    }
+
+    /**
+     * Removes an SRP service for the SRP client.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param notifyServer whether to notify SRP server about the removal
+     */
+    public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
+        String verb = notifyServer ? "remove" : "clear";
+        executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
+    }
+
+    /**
+     * Updates an existing SRP service for the SRP client.
+     *
+     * <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param subtypes the service subtypes like "_sub1"
+     * @param port the port number in range [1, 65535]
+     * @param txtMap the map of TXT names and values
+     * @throws TimeoutException if the service isn't updated within timeout
+     */
+    public void updateSrpService(
+            String serviceName,
+            String serviceType,
+            List<String> subtypes,
+            int port,
+            Map<String, byte[]> txtMap)
+            throws TimeoutException {
+        removeSrpService(serviceName, serviceType, false /* notifyServer */);
+        addSrpService(serviceName, serviceType, subtypes, port, txtMap);
+    }
+
+    /** Checks if an SRP service is registered. */
+    public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
+        List<String> lines = executeCommand("srp client service");
+        for (String line : lines) {
+            if (line.contains(serviceName) && line.contains(serviceType)) {
+                return line.contains("Registered");
+            }
+        }
+        return false;
+    }
+
+    /** Checks if an SRP host is registered. */
+    public boolean isSrpHostRegistered() {
+        List<String> lines = executeCommand("srp client host");
+        for (String line : lines) {
+            return line.contains("Registered");
+        }
+        return false;
+    }
+
     /** Runs the "factoryreset" command on the device. */
     public void factoryReset() {
         try {
@@ -240,6 +356,11 @@
         ping(address, null, 100 /* size */, 1 /* count */);
     }
 
+    @FormatMethod
+    private List<String> executeCommand(String commandFormat, Object... args) {
+        return executeCommand(String.format(commandFormat, args));
+    }
+
     private List<String> executeCommand(String command) {
         try {
             mWriter.write(command + "\n");
@@ -263,7 +384,7 @@
             if (line.equals("Done")) {
                 break;
             }
-            if (line.startsWith("Error:")) {
+            if (line.startsWith("Error")) {
                 fail("ot-cli-ftd reported an error: " + line);
             }
             if (!line.startsWith("> ")) {
@@ -272,4 +393,27 @@
         }
         return result;
     }
+
+    private static String txtMapToHexString(Map<String, byte[]> txtMap) {
+        if (txtMap == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
+            int length = entry.getKey().length() + entry.getValue().length + 1;
+            sb.append(String.format("%02x", length));
+            sb.append(toHexString(entry.getKey()));
+            sb.append(toHexString("="));
+            sb.append(toHexString(entry.getValue()));
+        }
+        return sb.toString();
+    }
+
+    private static String toHexString(String s) {
+        return toHexString(s.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static String toHexString(byte[] bytes) {
+        return base16().encode(bytes);
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 74251a6..6e70d24 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -23,12 +23,18 @@
 
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
 import android.net.TestNetworkInterface;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
 import android.net.thread.ThreadNetworkController;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 
+import androidx.annotation.NonNull;
+
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv6Header;
@@ -51,6 +57,7 @@
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -66,6 +73,7 @@
     public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
     public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
     public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+    public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
 
     private IntegrationTestUtils() {}
 
@@ -289,4 +297,106 @@
         }
         return false;
     }
+
+    /** Return the first discovered service of {@code serviceType}. */
+    public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
+            throws Exception {
+        CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
+        NsdManager.DiscoveryListener listener =
+                new DefaultDiscoveryListener() {
+                    @Override
+                    public void onServiceFound(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        try {
+            serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            nsdManager.stopServiceDiscovery(listener);
+        }
+
+        return serviceInfoFuture.get();
+    }
+
+    /**
+     * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
+     */
+    public static NsdManager.DiscoveryListener discoverForServiceLost(
+            NsdManager nsdManager,
+            String serviceType,
+            CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+        NsdManager.DiscoveryListener listener =
+                new DefaultDiscoveryListener() {
+                    @Override
+                    public void onServiceLost(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        return listener;
+    }
+
+    /** Resolves the service. */
+    public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
+            throws Exception {
+        return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
+    }
+
+    /** Returns the first resolved service that satisfies the {@code predicate}. */
+    public static NsdServiceInfo resolveServiceUntil(
+            NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
+            throws Exception {
+        CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
+        NsdManager.ServiceInfoCallback callback =
+                new DefaultServiceInfoCallback() {
+                    @Override
+                    public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+                        if (predicate.test(serviceInfo)) {
+                            resolvedServiceInfoFuture.complete(serviceInfo);
+                        }
+                    }
+                };
+        nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
+        try {
+            return resolvedServiceInfoFuture.get(
+                    SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            nsdManager.unregisterServiceInfoCallback(callback);
+        }
+    }
+
+    private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+        @Override
+        public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
+
+        @Override
+        public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
+
+        @Override
+        public void onDiscoveryStarted(String serviceType) {}
+
+        @Override
+        public void onDiscoveryStopped(String serviceType) {}
+
+        @Override
+        public void onServiceFound(NsdServiceInfo serviceInfo) {}
+
+        @Override
+        public void onServiceLost(NsdServiceInfo serviceInfo) {}
+    }
+
+    private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+        @Override
+        public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
+
+        @Override
+        public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
+
+        @Override
+        public void onServiceLost() {}
+
+        @Override
+        public void onServiceInfoCallbackUnregistered() {}
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 8aea0a3..54e89b1 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.net.InetAddresses;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Handler;
@@ -42,6 +43,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -282,6 +285,189 @@
     }
 
     @Test
+    public void registerHost_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mTestLooper.dispatchAll();
+
+        assertThat(actualServiceInfo.getServiceName()).isNull();
+        assertThat(actualServiceInfo.getServiceType()).isNull();
+        assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+        assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+        assertThat(actualServiceInfo.getAttributes()).isEmpty();
+        assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+        assertThat(actualServiceInfo.getHostAddresses())
+                .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+        verify(mRegistrationReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void registerHost_nsdManagerFails_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(),
+                        actualRegistrationListenerCaptor.capture());
+        mTestLooper.dispatchAll();
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+        mTestLooper.dispatchAll();
+
+        assertThat(actualServiceInfo.getServiceName()).isNull();
+        assertThat(actualServiceInfo.getServiceType()).isNull();
+        assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+        assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+        assertThat(actualServiceInfo.getAttributes()).isEmpty();
+        assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+        assertThat(actualServiceInfo.getHostAddresses())
+                .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void registerHost_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        doThrow(new IllegalArgumentException("NsdManager fails"))
+                .when(mMockNsdManager)
+                .registerService(any(), anyInt(), any(Executor.class), any());
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void unregisterHost_nsdManagerSucceeds_serviceUnregistrationSucceeds() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+        mTestLooper.dispatchAll();
+        verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+        actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
+        mTestLooper.dispatchAll();
+        verify(mUnregistrationReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void unregisterHost_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+        mTestLooper.dispatchAll();
+        verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+        actualRegistrationListener.onUnregistrationFailed(
+                actualServiceInfo, FAILURE_INTERNAL_ERROR);
+        mTestLooper.dispatchAll();
+        verify(mUnregistrationReceiver, times(1)).onError(0);
+    }
+
+    @Test
     public void onOtDaemonDied_unregisterAll() {
         prepareTest();
 
@@ -336,11 +522,30 @@
                 actualRegistrationListenerCaptor.getAllValues().get(1);
         actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
 
+        mNsdPublisher.registerHost(
+                "Myhost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                18 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(3))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+        NsdManager.RegistrationListener actualListener3 =
+                actualRegistrationListenerCaptor.getAllValues().get(1);
+        actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
         mNsdPublisher.onOtDaemonDied();
         mTestLooper.dispatchAll();
 
         verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
         verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
+        verify(mMockNsdManager, times(1)).unregisterService(actualListener3);
     }
 
     private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
@@ -356,6 +561,15 @@
         return txtAttribute;
     }
 
+    private static List<InetAddress> makeAddresses(String... addressStrings) {
+        List<InetAddress> addresses = new ArrayList<>();
+
+        for (String addressString : addressStrings) {
+            addresses.add(InetAddresses.parseNumericAddress(addressString));
+        }
+        return addresses;
+    }
+
     // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
     // thread looper, so TestLooper needs to be created inside each test case to install the
     // correct looper.