Merge "Add listeners for CertificateTranspaency DeviceConfig flags" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 136dfb1..ac4d8b1 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -22,6 +22,12 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -48,6 +54,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -419,4 +426,46 @@
         }
         return false;
     }
+
+    /**
+     * Build NetworkTemplate for the given upstream type.
+     *
+     * <p> NetworkTemplate.Builder API was introduced in Android T.
+     *
+     * @param type the upstream type
+     * @return A NetworkTemplate object with a corresponding match rule or null if tethering
+     * metrics' data usage cannot be collected for a given upstream type.
+     */
+    @Nullable
+    public static NetworkTemplate buildNetworkTemplateForUpstreamType(@NonNull UpstreamType type) {
+        if (!isUsageSupportedForUpstreamType(type)) return null;
+
+        switch (type) {
+            case UT_CELLULAR:
+                // TODO: Handle the DUN connection, which is not a default network.
+                return new NetworkTemplate.Builder(MATCH_MOBILE)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_WIFI:
+                return new NetworkTemplate.Builder(MATCH_WIFI)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_BLUETOOTH:
+                return new NetworkTemplate.Builder(MATCH_BLUETOOTH)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            case UT_ETHERNET:
+                return new NetworkTemplate.Builder(MATCH_ETHERNET)
+                        .setMeteredness(METERED_YES)
+                        .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+                        .build();
+            default:
+                Log.e(TAG, "Unsupported UpstreamType: " + type.name());
+                break;
+        }
+        return null;
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index 7cef9cb..fbc2893 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -22,6 +22,10 @@
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_NCM;
@@ -47,11 +51,14 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
+import android.os.Build;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -62,8 +69,11 @@
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
 import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -72,12 +82,15 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public final class TetheringMetricsTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
     private static final String SETTINGS_PKG = "com.android.settings";
     private static final String SYSTEMUI_PKG = "com.android.systemui";
     private static final String GMS_PKG = "com.google.android.gms";
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
+    private static final int MATCH_NONE = -1;
 
     @Mock private Context mContext;
     @Mock private Dependencies mDeps;
@@ -392,4 +405,27 @@
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
     }
+
+    private void runBuildNetworkTemplateForUpstreamType(final UpstreamType upstreamType,
+            final int matchRule)  {
+        final NetworkTemplate template =
+                TetheringMetrics.buildNetworkTemplateForUpstreamType(upstreamType);
+        if (matchRule == MATCH_NONE) {
+            assertNull(template);
+        } else {
+            assertEquals(matchRule, template.getMatchRule());
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testBuildNetworkTemplateForUpstreamType() {
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_CELLULAR, MATCH_MOBILE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI, MATCH_WIFI);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_BLUETOOTH, MATCH_BLUETOOTH);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_ETHERNET, MATCH_ETHERNET);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
+        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
+    }
 }
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index ac02229..b95e3b1 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -41,6 +41,7 @@
 import android.net.NetworkCapabilities;
 import android.net.ResolverParamsParcel;
 import android.net.Uri;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Binder;
 import android.os.RemoteException;
@@ -52,16 +53,17 @@
 import android.util.Pair;
 
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
 
 /**
  * Encapsulate the management of DNS settings for networks.
@@ -382,15 +384,14 @@
         paramsParcel.domains = getDomainStrings(lp.getDomains());
         paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
         paramsParcel.tlsServers =
-                strictMode ? makeStrings(
-                        Arrays.stream(privateDnsCfg.ips)
-                              .filter((ip) -> lp.isReachable(ip))
-                              .collect(Collectors.toList()))
+                strictMode ? makeStrings(getReachableAddressList(privateDnsCfg.ips, lp))
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
         paramsParcel.transportTypes = nc.getTransportTypes();
         paramsParcel.meteredNetwork = nc.isMetered();
         paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
+        paramsParcel.dohParams = makeDohParamsParcel(privateDnsCfg, lp);
+
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
         if (useTls) {
@@ -404,15 +405,16 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
+                + "%d, %d, %s, %s, %s, %b, %s, %s, %s, %s, %d)", paramsParcel.netId,
                 Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
                 paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
                 paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
                 paramsParcel.retryCount, paramsParcel.tlsName,
                 Arrays.toString(paramsParcel.tlsServers),
                 Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
-                Arrays.toString(paramsParcel.interfaceNames)));
-
+                Arrays.toString(paramsParcel.interfaceNames),
+                paramsParcel.dohParams.name, Arrays.toString(paramsParcel.dohParams.ips),
+                paramsParcel.dohParams.dohpath, paramsParcel.dohParams.port));
         try {
             mDnsResolver.setResolverConfiguration(paramsParcel);
         } catch (RemoteException | ServiceSpecificException e) {
@@ -498,4 +500,26 @@
     private static String[] getDomainStrings(String domains) {
         return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
     }
+
+    @NonNull
+    private List<InetAddress> getReachableAddressList(@NonNull InetAddress[] ips,
+            @NonNull LinkProperties lp) {
+        final ArrayList<InetAddress> out = new ArrayList<InetAddress>(Arrays.asList(ips));
+        out.removeIf(ip -> !lp.isReachable(ip));
+        return out;
+    }
+
+    @NonNull
+    private DohParamsParcel makeDohParamsParcel(@NonNull PrivateDnsConfig cfg,
+            @NonNull LinkProperties lp) {
+        if (cfg.mode == PRIVATE_DNS_MODE_OFF) {
+            return new DohParamsParcel.Builder().build();
+        }
+        return new DohParamsParcel.Builder()
+                .setName(cfg.dohName)
+                .setIps(makeStrings(getReachableAddressList(cfg.dohIps, lp)))
+                .setDohpath(cfg.dohPath)
+                .setPort(cfg.dohPort)
+                .build();
+    }
 }
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ed0670d..94a2411 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -296,6 +296,9 @@
         "framework-connectivity-t.stubs.module_lib",
         "framework-location.stubs.module_lib",
     ],
+    static_libs: [
+        "modules-utils-build",
+    ],
     jarjar_rules: "jarjar-rules-shared.txt",
     visibility: [
         "//cts/tests/tests/net",
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
index 41a9428..04b6174 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -19,6 +19,9 @@
 import android.app.usage.NetworkStats;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.ArrayList;
 
 /**
  * Various utilities used for NetworkStats related code.
@@ -105,12 +108,21 @@
      */
     public static android.net.NetworkStats fromPublicNetworkStats(
             NetworkStats publiceNetworkStats) {
-        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
+        final ArrayList<android.net.NetworkStats.Entry> entries = new ArrayList<>();
         while (publiceNetworkStats.hasNextBucket()) {
             NetworkStats.Bucket bucket = new NetworkStats.Bucket();
             publiceNetworkStats.getNextBucket(bucket);
-            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
-            stats = stats.addEntry(entry);
+            entries.add(fromBucket(bucket));
+        }
+        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
+        // The new API is only supported on devices running the mainline version of `NetworkStats`.
+        // It should always be used when available for memory efficiency.
+        if (SdkLevel.isAtLeastT()) {
+            stats = stats.addEntries(entries);
+        } else {
+            for (android.net.NetworkStats.Entry entry : entries) {
+                stats = stats.addEntry(entry);
+            }
         }
         return stats;
     }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index 2785ea9..97d3c19 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -17,15 +17,27 @@
 package com.android.net.module.util
 
 import android.net.NetworkStats
-import android.text.TextUtils
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.DEFAULT_NETWORK_YES
+import android.net.NetworkStats.Entry
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
+import com.android.testutils.assertEntryEquals
+import com.android.testutils.assertNetworkStatsEquals
+import com.android.testutils.makePublicStatsFromAndroidNetStats
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -90,11 +102,40 @@
             NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
             0 /* operations */)
 
-        // TODO: Use assertEquals once all downstreams accept null iface in
-        // NetworkStats.Entry#equals.
         assertEntryEquals(expectedEntry, entry)
     }
 
+    @Test
+    fun testPublicStatsToAndroidNetStats() {
+        val uid1 = 10001
+        val uid2 = 10002
+        val testIface = "wlan0"
+        val testAndroidNetStats = NetworkStats(0L, 3)
+                .addEntry(Entry(testIface, uid1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3))
+                .addEntry(Entry(
+                        testIface, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 2, 7, 2, 5, 7))
+                .addEntry(Entry(testIface, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 4, 5, 3, 1, 8))
+        val publicStats: android.app.usage.NetworkStats =
+                makePublicStatsFromAndroidNetStats(testAndroidNetStats)
+        val androidNetStats: NetworkStats =
+                NetworkStatsUtils.fromPublicNetworkStats(publicStats)
+
+        // 1. The public `NetworkStats` class does not include interface information.
+        //    Interface details must be removed and items with duplicated
+        //    keys need to be merged before making any comparisons.
+        // 2. The public `NetworkStats` class lacks an operations field.
+        //    Thus, the information will not be preserved during the conversion.
+        val expectedStats = NetworkStats(0L, 2)
+                .addEntry(Entry(null, uid1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 0))
+                .addEntry(Entry(null, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 6, 12, 5, 6, 0))
+        assertNetworkStatsEquals(expectedStats, androidNetStats)
+    }
+
     private fun makeMockBucket(
         uid: Int,
         tag: Int,
@@ -121,22 +162,4 @@
         doReturn(txPackets).`when`(ret).getTxPackets()
         return ret
     }
-
-    /**
-     * Assert that the two {@link NetworkStats.Entry} are equals.
-     */
-    private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
-        TextUtils.equals(left.iface, right.iface)
-        assertEquals(left.uid, right.uid)
-        assertEquals(left.set, right.set)
-        assertEquals(left.tag, right.tag)
-        assertEquals(left.metered, right.metered)
-        assertEquals(left.roaming, right.roaming)
-        assertEquals(left.defaultNetwork, right.defaultNetwork)
-        assertEquals(left.rxBytes, right.rxBytes)
-        assertEquals(left.rxPackets, right.rxPackets)
-        assertEquals(left.txBytes, right.txBytes)
-        assertEquals(left.txPackets, right.txPackets)
-        assertEquals(left.operations, right.operations)
-    }
 }
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
index 2f436cd..57920fc 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
@@ -31,7 +31,7 @@
 import org.junit.runners.JUnit4
 
 private const val TEST_IFACE = "test0"
-private const val TEST_IFACE2 = "test2"
+private val TEST_IFACE2: String? = null
 private const val TEST_START = 1194220800000L
 
 @RunWith(JUnit4::class)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index 8324b25..e8c7297 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -16,8 +16,20 @@
 
 package com.android.testutils
 
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.INetworkStatsService
+import android.net.INetworkStatsSession
 import android.net.NetworkStats
+import android.net.NetworkTemplate
+import android.text.TextUtils
+import com.android.modules.utils.build.SdkLevel
 import kotlin.test.assertTrue
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
 
 @JvmOverloads
 fun orderInsensitiveEquals(
@@ -26,7 +38,7 @@
     compareTime: Boolean = false
 ): Boolean {
     if (leftStats == rightStats) return true
-    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+    if (compareTime && leftStats.elapsedRealtime != rightStats.elapsedRealtime) {
         return false
     }
 
@@ -47,12 +59,41 @@
                 left.metered, left.roaming, left.defaultNetwork, i)
         if (j == -1) return false
         rightTrimmedEmpty.getValues(j, right)
-        if (left != right) return false
+        if (SdkLevel.isAtLeastT()) {
+            if (left != right) return false
+        } else {
+            if (!checkEntryEquals(left, right)) return false
+        }
     }
     return true
 }
 
 /**
+ * Assert that the two {@link NetworkStats.Entry} are equals.
+ */
+fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+    assertTrue(checkEntryEquals(left, right))
+}
+
+// TODO: Make all callers use NetworkStats.Entry#equals once S- downstreams
+//  are no longer supported. Because NetworkStats is mainlined on T+ and
+//  NetworkStats.Entry#equals in S- does not support null iface.
+fun checkEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry): Boolean {
+    return TextUtils.equals(left.iface, right.iface) &&
+            left.uid == right.uid &&
+            left.set == right.set &&
+            left.tag == right.tag &&
+            left.metered == right.metered &&
+            left.roaming == right.roaming &&
+            left.defaultNetwork == right.defaultNetwork &&
+            left.rxBytes == right.rxBytes &&
+            left.rxPackets == right.rxPackets &&
+            left.txBytes == right.txBytes &&
+            left.txPackets == right.txPackets &&
+            left.operations == right.operations
+}
+
+/**
  * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
  * necessarily the same.
  *
@@ -66,7 +107,7 @@
     compareTime: Boolean = false
 ) {
     assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
-            "expected: " + expected + " but was: " + actual)
+            "expected: $expected but was: $actual")
 }
 
 /**
@@ -74,5 +115,34 @@
  * object.
  */
 fun assertParcelingIsLossless(stats: NetworkStats) {
-    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+    assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
+}
+
+/**
+ * Make a {@link android.app.usage.NetworkStats} instance from
+ * a {@link android.net.NetworkStats} instance.
+ */
+// It's not possible to directly create a mocked `NetworkStats` instance
+// because of limitations with `NetworkStats#getNextBucket`.
+// As a workaround for testing, create a mock by controlling the return values
+// from the mocked service that provides the `NetworkStats` data.
+// Notes:
+//   1. The order of records in the final `NetworkStats` object might change or
+//      some records might be merged if there are items with duplicate keys.
+//   2. The interface and operations fields will be empty since there is
+//      no such field in the {@link android.app.usage.NetworkStats}.
+fun makePublicStatsFromAndroidNetStats(androidNetStats: NetworkStats):
+        android.app.usage.NetworkStats {
+    val mockService = Mockito.mock(INetworkStatsService::class.java)
+    val manager = NetworkStatsManager(Mockito.mock(Context::class.java), mockService)
+    val mockStatsSession = Mockito.mock(INetworkStatsSession::class.java)
+
+    Mockito.doReturn(mockStatsSession).`when`(mockService)
+            .openSessionForUsageStats(anyInt(), any())
+    Mockito.doReturn(androidNetStats).`when`(mockStatsSession).getSummaryForAllUid(
+            any(NetworkTemplate::class.java), anyLong(), anyLong(), anyBoolean())
+    return manager.querySummary(
+            Mockito.mock(NetworkTemplate::class.java),
+            Long.MIN_VALUE, Long.MAX_VALUE
+    )
 }
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
index 8b4ffa5..ef6af75 100644
--- a/staticlibs/testutils/host/python/wifip2p_utils.py
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -14,11 +14,13 @@
 
 from mobly import asserts
 from mobly.controllers import android_device
+from net_tests_utils.host.python import tether_utils
 
 
 def assume_wifi_p2p_test_preconditions(
     server_device: android_device, client_device: android_device
 ) -> None:
+  """Preconditions check for running Wi-Fi P2P test."""
   server = server_device.connectivity_multi_devices_snippet
   client = client_device.connectivity_multi_devices_snippet
 
@@ -36,10 +38,51 @@
 def setup_wifi_p2p_server_and_client(
     server_device: android_device, client_device: android_device
 ) -> None:
-  """Set up the Wi-Fi P2P server and client."""
+  """Set up the Wi-Fi P2P server and client, then connect them to establish a Wi-Fi P2P connection."""
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
   # Start Wi-Fi P2P on both server and client.
-  server_device.connectivity_multi_devices_snippet.startWifiP2p()
-  client_device.connectivity_multi_devices_snippet.startWifiP2p()
+  server.startWifiP2p()
+  client.startWifiP2p()
+
+  # Get the current device name
+  server_name = server.getDeviceName()
+  client_name = client.getDeviceName()
+
+  # Generate Wi-Fi P2P group passphrase with random characters.
+  group_name = "DIRECT-" + tether_utils.generate_uuid32_base64()
+  group_passphrase = tether_utils.generate_uuid32_base64()
+
+  # Server creates a Wi-Fi P2P group
+  server.createGroup(group_name, group_passphrase)
+
+  # Start Wi-Fi P2p peers discovery on both devices
+  server.startPeersDiscovery()
+  client.startPeersDiscovery()
+
+  # Ensure the target device has been discovered
+  server_address = client.ensureDeviceDiscovered(server_name)
+  client_address = server.ensureDeviceDiscovered(client_name)
+
+  # Server invites the device to the group
+  server.inviteDeviceToGroup(group_name, group_passphrase, client_address)
+
+  # Wait for a p2p connection changed intent to ensure the invitation has been
+  # received.
+  client.waitForP2pConnectionChanged(True, group_name)
+  # Accept the group invitation
+  client.acceptGroupInvitation(server_address)
+
+  # Server waits for connection request from client and accept joining
+  server.waitForPeerConnectionRequestAndAcceptJoining(client_address)
+
+  # Wait for a p2p connection changed intent to ensure joining the group
+  client.waitForP2pConnectionChanged(False, group_name)
+
+  # Ensure Wi-Fi P2P connected on both devices
+  client.ensureDeviceConnected(server_name)
+  server.ensureDeviceConnected(client_name)
 
 
 def cleanup_wifi_p2p(
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index eceb535..416a2e8 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -13,6 +13,7 @@
 #  limitations under the License.
 
 from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python import wifip2p_utils
 from net_tests_utils.host.python.tether_utils import UpstreamType
 
 
@@ -68,3 +69,21 @@
       tether_utils.cleanup_tethering_for_upstream_type(
           self.serverDevice, UpstreamType.NONE
       )
+
+  def test_mdns_via_wifip2p(self):
+    wifip2p_utils.assume_wifi_p2p_test_preconditions(
+        self.serverDevice, self.clientDevice
+    )
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
+    try:
+      wifip2p_utils.setup_wifi_p2p_server_and_client(
+          self.serverDevice, self.clientDevice
+      )
+      mdns_utils.register_mdns_service_and_discover_resolve(
+          self.clientDevice, self.serverDevice
+      )
+    finally:
+      mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+      wifip2p_utils.cleanup_wifi_p2p(self.serverDevice, self.clientDevice)
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
index e0929bb..f8c9351 100644
--- a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -16,13 +16,28 @@
 
 package com.google.snippet.connectivity
 
+import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.MacAddress
 import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pConfig
+import android.net.wifi.p2p.WifiP2pDevice
+import android.net.wifi.p2p.WifiP2pDeviceList
+import android.net.wifi.p2p.WifiP2pGroup
 import android.net.wifi.p2p.WifiP2pManager
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
 import kotlin.test.fail
 
 private const val TIMEOUT_MS = 60000L
@@ -38,6 +53,35 @@
                 ?: fail("Could not get WifiP2pManager service")
     }
     private lateinit var wifip2pChannel: WifiP2pManager.Channel
+    private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
+
+    private class Wifip2pIntentReceiver : BroadcastReceiver() {
+        val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
+
+        sealed class IntentReceivedEvent {
+            abstract val intent: Intent
+            data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
+            data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
+        }
+
+        override fun onReceive(context: Context, intent: Intent) {
+            when (intent.action) {
+                WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
+                    history.add(ConnectionChanged(intent))
+                }
+                WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
+                    history.add(PeersChanged(intent))
+                }
+            }
+        }
+
+        inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
+                timeoutMs: Long = TIMEOUT_MS,
+                crossinline predicate: (T) -> Boolean = { true }
+        ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
+            assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
+        } as T
+    }
 
     @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
     fun isP2pSupported() = wifiManager.isP2pSupported()
@@ -55,6 +99,10 @@
             }
         }
         p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        // Register an intent filter to receive Wi-Fi P2P intents
+        val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
+        filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
+        context.registerReceiver(wifip2pIntentReceiver, filter)
     }
 
     @Rpc(description = "Stop Wi-Fi P2P")
@@ -63,5 +111,202 @@
             wifip2pManager.cancelConnect(wifip2pChannel, null)
             wifip2pManager.removeGroup(wifip2pChannel, null)
         }
+        // Unregister the intent filter
+        context.unregisterReceiver(wifip2pIntentReceiver)
+    }
+
+    @Rpc(description = "Get the current device name")
+    fun getDeviceName(): String {
+        // Retrieve current device info
+        val deviceFuture = CompletableFuture<String>()
+        wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
+            if (wifiP2pDevice != null) {
+                deviceFuture.complete(wifiP2pDevice.deviceName)
+            }
+        }
+        // Return current device name
+        return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    @Rpc(description = "Wait for a p2p connection changed intent and check the group")
+    @Suppress("DEPRECATION")
+    fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
+        wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
+            val p2pGroup: WifiP2pGroup? =
+                    it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
+            val groupMatched = p2pGroup?.networkName == groupName
+            return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
+        }
+    }
+
+    @Rpc(description = "Create a Wi-Fi P2P group")
+    fun createGroup(groupName: String, groupPassphrase: String) {
+        // Create a Wi-Fi P2P group
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .build()
+        val createGroupFuture = CompletableFuture<Boolean>()
+        wifip2pManager.createGroup(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() { createGroupFuture.complete(true) }
+                }
+        )
+        createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+        // Ensure the Wi-Fi P2P group is created.
+        waitForP2pConnectionChanged(false, groupName)
+    }
+
+    @Rpc(description = "Start Wi-Fi P2P peers discovery")
+    fun startPeersDiscovery() {
+        // Start discovery Wi-Fi P2P peers
+        wifip2pManager.discoverPeers(wifip2pChannel, null)
+
+        // Ensure the discovery is started
+        val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+                p2pDiscoveryStartedFuture.complete(true)
+            }
+        }
+        p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    /**
+     * Get the device address from the given intent that matches the given device name.
+     *
+     * @param peersChangedIntent the intent to get the device address from
+     * @param deviceName the target device name
+     * @return the address of the target device or null if no devices match.
+     */
+    @Suppress("DEPRECATION")
+    private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
+        val peers: WifiP2pDeviceList? =
+                peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
+        return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
+    }
+
+    /**
+     * Ensure the given device has been discovered and returns the associated device address for
+     * connection.
+     *
+     * @param deviceName the target device name
+     * @return the address of the target device.
+     */
+    @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
+    fun ensureDeviceDiscovered(deviceName: String): String {
+        val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
+            return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
+        }
+        return getDeviceAddress(changedEvent.intent, deviceName)
+                ?: fail("Missing device in filtered intent")
+    }
+
+    @Rpc(description = "Invite a Wi-Fi P2P device to the group")
+    fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
+        // Connect to the device to send invitation
+        val wifip2pConfig = WifiP2pConfig.Builder()
+                .setNetworkName(groupName)
+                .setPassphrase(groupPassphrase)
+                .setDeviceAddress(MacAddress.fromString(deviceAddress))
+                .build()
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.connect(
+                wifip2pChannel,
+                wifip2pConfig,
+                object : WifiP2pManager.ActionListener {
+                    override fun onFailure(reason: Int) = Unit
+                    override fun onSuccess() {
+                        connectedFuture.complete(true)
+                    }
+                }
+        )
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun runExternalApproverForGroupProcess(
+            deviceAddress: String,
+            isGroupInvitation: Boolean
+    ) {
+        val peer = MacAddress.fromString(deviceAddress)
+        runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
+            val connectionRequestFuture = CompletableFuture<Boolean>()
+            val attachedFuture = CompletableFuture<Boolean>()
+            wifip2pManager.addExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ExternalApproverRequestListener {
+                        override fun onAttached(deviceAddress: MacAddress) {
+                            attachedFuture.complete(true)
+                        }
+                        override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
+                        override fun onConnectionRequested(
+                                requestType: Int,
+                                config: WifiP2pConfig,
+                                device: WifiP2pDevice
+                        ) {
+                            connectionRequestFuture.complete(true)
+                        }
+                        override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
+                    }
+            )
+            if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
+                connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val resultFuture = CompletableFuture<Boolean>()
+            wifip2pManager.setConnectionRequestResult(
+                    wifip2pChannel,
+                    peer,
+                    WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            resultFuture.complete(true)
+                        }
+                    }
+            )
+            resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+            val removeFuture = CompletableFuture<Boolean>()
+            wifip2pManager.removeExternalApprover(
+                    wifip2pChannel,
+                    peer,
+                    object : WifiP2pManager.ActionListener {
+                        override fun onFailure(reason: Int) = Unit
+                        override fun onSuccess() {
+                            removeFuture.complete(true)
+                        }
+                    }
+            )
+            removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
+    }
+
+    @Rpc(description = "Accept P2P group invitation from device")
+    fun acceptGroupInvitation(deviceAddress: String) {
+        // Accept the Wi-Fi P2P group invitation
+        runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Wait for connection request from the peer and accept joining")
+    fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
+        // Wait for connection request from the peer and accept joining
+        runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
+    }
+
+    @Rpc(description = "Ensure the target device is connected")
+    fun ensureDeviceConnected(deviceName: String) {
+        // Retrieve peers and ensure the target device is connected
+        val connectedFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
+            it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
+                connectedFuture.complete(true)
+            }
+        }
+        connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index ea3d2dd..b47b97d 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -54,6 +54,7 @@
 import android.net.ResolverOptionsParcel;
 import android.net.ResolverParamsParcel;
 import android.net.RouteInfo;
+import android.net.resolv.aidl.DohParamsParcel;
 import android.net.shared.PrivateDnsConfig;
 import android.os.Build;
 import android.provider.Settings;
@@ -327,8 +328,16 @@
     @Test
     public void testSendDnsConfiguration() throws Exception {
         reset(mMockDnsResolver);
-        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
-                mDnsManager.getPrivateDnsConfig());
+        final PrivateDnsConfig cfg = new PrivateDnsConfig(
+                PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+                null /* hostname */,
+                null /* ips */,
+                "doh.com" /* dohName */,
+                null /* dohIps */,
+                "/some-path{?dns}" /* dohPath */,
+                5353 /* dohPort */);
+
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID), cfg);
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(TEST_IFACENAME);
         lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
@@ -352,7 +361,11 @@
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
         expectedParams.meteredNetwork = true;
-        expectedParams.dohParams = null;
+        expectedParams.dohParams = new DohParamsParcel.Builder()
+                .setName("doh.com")
+                .setDohpath("/some-path{?dns}")
+                .setPort(5353)
+                .build();
         expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
         verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
     }