Merge "Remove duplicated bpf offload support check in IpServer" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index d3b01ea..bb3dc24 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -110,7 +110,6 @@
     ],
     apps: [
         "ServiceConnectivityResources",
-        "HalfSheetUX",
     ],
     prebuilts: ["current_sdkinfo"],
     manifest: "manifest.json",
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 74b09e7..e2e6d02 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -92,6 +92,8 @@
 DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
 DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
+                       INGRESS_DISCARD_MAP_SIZE)
 
 /* never actually used from ebpf */
 DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
@@ -343,6 +345,35 @@
     return *config;
 }
 
+static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
+                                                          const unsigned kver) {
+    // Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
+    // provides relative to L3 header reads.  Without that we could fetch the wrong bytes.
+    // Additionally earlier bpf verifiers are much harder to please.
+    if (kver < KVER(4, 19, 0)) return false;
+
+    IngressDiscardKey k = {};
+    if (skb->protocol == htons(ETH_P_IP)) {
+        k.daddr.s6_addr32[2] = htonl(0xFFFF);
+        (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(daddr), &k.daddr.s6_addr32[3], 4, kver);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(daddr), &k.daddr, sizeof(k.daddr), kver);
+    } else {
+        return false; // non IPv4/IPv6, so no IP to match on
+    }
+
+    // we didn't check for load success, because destination bytes will be zeroed if
+    // bpf_skb_load_bytes_net() fails, instead we rely on daddr of '::' and '::ffff:0.0.0.0'
+    // never being present in the map itself
+
+    IngressDiscardValue* v = bpf_ingress_discard_map_lookup_elem(&k);
+    if (!v) return false;  // lookup failure -> no protection in place -> allow
+    // if (skb->ifindex == 1) return false;  // allow 'lo', but can't happen - see callsite
+    if (skb->ifindex == v->iif[0]) return false;  // allowed interface
+    if (skb->ifindex == v->iif[1]) return false;  // allowed interface
+    return true;  // disallowed interface
+}
+
 // DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
 #define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
 // DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
@@ -368,6 +399,7 @@
     if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return DROP;
 
     if (!egress && skb->ifindex != 1) {
+        if (ingress_should_discard(skb, kver)) return DROP;
         if (uidRules & IIF_MATCH) {
             if (allowed_iif && skb->ifindex != allowed_iif) {
                 // Drops packets not coming from lo nor the allowed interface
@@ -413,7 +445,8 @@
     // Always allow and never count clat traffic. Only the IPv4 traffic on the stacked
     // interface is accounted for and subject to usage restrictions.
     // CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
-    if (uid == AID_CLAT) return PASS;
+    // CLAT daemon receives via an untagged AF_PACKET socket.
+    if (egress && uid == AID_CLAT) return PASS;
 
     int match = bpf_owner_match(skb, sock_uid, egress, kver);
 
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index dcf6d6a..836e998 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -122,6 +122,7 @@
 static const int IFACE_STATS_MAP_SIZE = 1000;
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 4000;
+static const int INGRESS_DISCARD_MAP_SIZE = 100;
 static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
 
 #ifdef __cplusplus
@@ -166,6 +167,7 @@
 #define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
 #define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
 #define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
+#define INGRESS_DISCARD_MAP_PATH BPF_NETD_PATH "map_netd_ingress_discard_map"
 #define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
 #define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
 
@@ -214,6 +216,18 @@
 } UidOwnerValue;
 STRUCT_SIZE(UidOwnerValue, 2 * 4);  // 8
 
+typedef struct {
+    // The destination ip of the incoming packet.  IPv4 uses IPv4-mapped IPv6 address format.
+    struct in6_addr daddr;
+} IngressDiscardKey;
+STRUCT_SIZE(IngressDiscardKey, 16);  // 16
+
+typedef struct {
+    // Allowed interface indexes.  Use same value multiple times if you just want to match 1 value.
+    uint32_t iif[2];
+} IngressDiscardValue;
+STRUCT_SIZE(IngressDiscardValue, 2 * 4);  // 8
+
 // Entry in the configuration map that stores which UID rules are enabled.
 #define UID_RULES_CONFIGURATION_KEY 0
 // Entry in the configuration map that stores which stats map is currently in use.
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index dacdaf2..5ae1ef9 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -19,6 +19,14 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+framework_remoteauth_srcs = [":framework-remoteauth-java-sources"]
+framework_remoteauth_api_srcs = []
+
+java_defaults {
+    name: "enable-remoteauth-targets",
+    enabled: true,
+}
+
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
@@ -43,8 +51,7 @@
         ":framework-connectivity-tiramisu-updatable-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
-        ":framework-remoteauth-java-sources",
-    ],
+    ] + framework_remoteauth_srcs,
     libs: [
         "unsupportedappusage",
         "app-compat-annotations",
@@ -115,6 +122,7 @@
         "framework-connectivity-t-defaults",
         "enable-framework-connectivity-t-targets",
     ],
+    api_srcs: framework_remoteauth_api_srcs,
     // Do not add static_libs to this library: put them in framework-connectivity instead.
     // The jarjar rules are only so that references to jarjared utils in
     // framework-connectivity-pre-jarjar match at runtime.
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 42c83d8..5a8d47b 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -207,43 +207,3 @@
 
 }
 
-package android.remoteauth {
-
-  public interface DeviceDiscoveryCallback {
-    method public void onDeviceUpdate(@NonNull android.remoteauth.RemoteDevice, int);
-    method public void onTimeout();
-    field public static final int STATE_LOST = 0; // 0x0
-    field public static final int STATE_SEEN = 1; // 0x1
-  }
-
-  public final class RemoteAuthFrameworkInitializer {
-    method public static void registerServiceWrappers();
-  }
-
-  public class RemoteAuthManager {
-    method public boolean isRemoteAuthSupported();
-    method public boolean startDiscovery(int, @NonNull java.util.concurrent.Executor, @NonNull android.remoteauth.DeviceDiscoveryCallback);
-    method public void stopDiscovery(@NonNull android.remoteauth.DeviceDiscoveryCallback);
-  }
-
-  public final class RemoteDevice implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public int getConnectionId();
-    method @Nullable public String getName();
-    method public int getRegistrationState();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.remoteauth.RemoteDevice> CREATOR;
-    field public static final int STATE_NOT_REGISTERED = 0; // 0x0
-    field public static final int STATE_REGISTERED = 1; // 0x1
-  }
-
-  public static final class RemoteDevice.Builder {
-    ctor public RemoteDevice.Builder(int);
-    method @NonNull public android.remoteauth.RemoteDevice build();
-    method @NonNull public android.remoteauth.RemoteDevice.Builder setConnectionId(int);
-    method @NonNull public android.remoteauth.RemoteDevice.Builder setName(@Nullable String);
-    method @NonNull public android.remoteauth.RemoteDevice.Builder setRegistrationState(int);
-  }
-
-}
-
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 7bd5a7d..2c839bc 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -161,6 +161,23 @@
     }
 
     /**
+     * Create a new OffloadServiceInfo with payload updated.
+     *
+     * @hide
+     */
+    @NonNull
+    public OffloadServiceInfo withOffloadPayload(@NonNull byte[] offloadPayload) {
+        return new OffloadServiceInfo(
+                this.getKey(),
+                this.getSubtypes(),
+                this.getHostname(),
+                offloadPayload,
+                this.getPriority(),
+                this.getOffloadType()
+        );
+    }
+
+    /**
      * Get the offloadType.
      * <p>
      * For example, if the {@link com.android.server.NsdService} requests the OffloadEngine to both
diff --git a/framework/Android.bp b/framework/Android.bp
index 813e296..e577e6d 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -80,9 +80,9 @@
     impl_only_libs: [
         // TODO: figure out why just using "framework-tethering" uses the stubs, even though both
         // framework-connectivity and framework-tethering are in the same APEX.
+        "framework-location.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
-        "net-utils-device-common",
     ],
     static_libs: [
         "mdns_aidl_interface-lateststable-java",
@@ -91,6 +91,9 @@
         "modules-utils-preconditions",
         "framework-connectivity-javastream-protos",
     ],
+    impl_only_static_libs: [
+        "net-utils-device-common-struct",
+    ],
     libs: [
         "androidx.annotation_annotation",
         "app-compat-annotations",
@@ -112,12 +115,20 @@
         "httpclient_api",
         "httpclient_impl",
         "http_client_logging",
+        // Framework-connectivity-pre-jarjar is identical to framework-connectivity
+        // implementation, but without the jarjar rules. However, framework-connectivity
+        // is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
+        // to generate the SDK stubs.
+        // Even if the library is included in "impl_only_static_libs" of defaults. This is still
+        // needed because java_library which doesn't understand "impl_only_static_libs".
+        "net-utils-device-common-struct",
     ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
         // to generate the connectivity stubs. That would create a circular dependency
         // because the tethering impl depend on the connectivity stubs (e.g.,
         // TetheringRequest depends on LinkAddress).
+        "framework-location.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
new file mode 100644
index 0000000..2191682
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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 android.util.Pair;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * BpfNetMaps related constants that can be shared among modules.
+ *
+ * @hide
+ */
+// Note that this class should be put into bootclasspath instead of static libraries.
+// Because modules could have different copies of this class if this is statically linked,
+// which would be problematic if the definitions in these modules are not synchronized.
+public class BpfNetMapsConstants {
+    // Prevent this class from being accidental instantiated.
+    private BpfNetMapsConstants() {}
+
+    public static final String CONFIGURATION_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+    public static final String UID_OWNER_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
+    public static final String UID_PERMISSION_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
+    public static final String COOKIE_TAG_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
+    public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
+    public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
+
+    // LINT.IfChange(match_type)
+    public static final long NO_MATCH = 0;
+    public static final long HAPPY_BOX_MATCH = (1 << 0);
+    public static final long PENALTY_BOX_MATCH = (1 << 1);
+    public static final long DOZABLE_MATCH = (1 << 2);
+    public static final long STANDBY_MATCH = (1 << 3);
+    public static final long POWERSAVE_MATCH = (1 << 4);
+    public static final long RESTRICTED_MATCH = (1 << 5);
+    public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+    public static final long IIF_MATCH = (1 << 7);
+    public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+    public static final long OEM_DENY_1_MATCH = (1 << 9);
+    public static final long OEM_DENY_2_MATCH = (1 << 10);
+    public static final long OEM_DENY_3_MATCH = (1 << 11);
+    // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
+
+    public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
+            Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
+            Pair.create(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH"),
+            Pair.create(DOZABLE_MATCH, "DOZABLE_MATCH"),
+            Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
+            Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
+            Pair.create(RESTRICTED_MATCH, "RESTRICTED_MATCH"),
+            Pair.create(LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"),
+            Pair.create(IIF_MATCH, "IIF_MATCH"),
+            Pair.create(LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"),
+            Pair.create(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"),
+            Pair.create(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"),
+            Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH")
+    );
+}
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
new file mode 100644
index 0000000..d464e3d
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.BpfNetMapsConstants.DOZABLE_MATCH;
+import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
+import static android.net.BpfNetMapsConstants.MATCH_LIST;
+import static android.net.BpfNetMapsConstants.NO_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
+import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
+import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
+import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.system.OsConstants.EINVAL;
+
+import android.os.ServiceSpecificException;
+import android.util.Pair;
+
+import java.util.StringJoiner;
+
+/**
+ * The classes and the methods for BpfNetMaps utilization.
+ *
+ * @hide
+ */
+// Note that this class should be put into bootclasspath instead of static libraries.
+// Because modules could have different copies of this class if this is statically linked,
+// which would be problematic if the definitions in these modules are not synchronized.
+public class BpfNetMapsUtils {
+    // Prevent this class from being accidental instantiated.
+    private BpfNetMapsUtils() {}
+
+    /**
+     * Get corresponding match from firewall chain.
+     */
+    public static long getMatchByFirewallChain(final int chain) {
+        switch (chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+                return DOZABLE_MATCH;
+            case FIREWALL_CHAIN_STANDBY:
+                return STANDBY_MATCH;
+            case FIREWALL_CHAIN_POWERSAVE:
+                return POWERSAVE_MATCH;
+            case FIREWALL_CHAIN_RESTRICTED:
+                return RESTRICTED_MATCH;
+            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return LOW_POWER_STANDBY_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_1:
+                return OEM_DENY_1_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_2:
+                return OEM_DENY_2_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_3:
+                return OEM_DENY_3_MATCH;
+            default:
+                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+        }
+    }
+
+    /**
+     * Get if the chain is allow list or not.
+     *
+     * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
+     * DENYLIST means the firewall allows all by default, uids must be explicitly denyed
+     */
+    public static boolean isFirewallAllowList(final int chain) {
+        switch (chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+            case FIREWALL_CHAIN_POWERSAVE:
+            case FIREWALL_CHAIN_RESTRICTED:
+            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return true;
+            case FIREWALL_CHAIN_STANDBY:
+            case FIREWALL_CHAIN_OEM_DENY_1:
+            case FIREWALL_CHAIN_OEM_DENY_2:
+            case FIREWALL_CHAIN_OEM_DENY_3:
+                return false;
+            default:
+                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+        }
+    }
+
+    /**
+     * Get match string representation from the given match bitmap.
+     */
+    public static String matchToString(long matchMask) {
+        if (matchMask == NO_MATCH) {
+            return "NO_MATCH";
+        }
+
+        final StringJoiner sj = new StringJoiner(" ");
+        for (final Pair<Long, String> match : MATCH_LIST) {
+            final long matchFlag = match.first;
+            final String matchName = match.second;
+            if ((matchMask & matchFlag) != 0) {
+                sj.add(matchName);
+                matchMask &= ~matchFlag;
+            }
+        }
+        if (matchMask != 0) {
+            sj.add("UNKNOWN_MATCH(" + matchMask + ")");
+        }
+        return sj.toString();
+    }
+}
diff --git a/nearby/TEST_MAPPING b/nearby/TEST_MAPPING
index d68bcc9..7e9a375 100644
--- a/nearby/TEST_MAPPING
+++ b/nearby/TEST_MAPPING
@@ -2,20 +2,17 @@
   "presubmit": [
     {
       "name": "NearbyUnitTests"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "NearbyUnitTests"
     },
     {
       "name": "NearbyIntegrationPrivilegedTests"
     },
     {
       "name": "NearbyIntegrationUntrustedTests"
-    },
-    {
-      "name": "NearbyIntegrationUiTests"
-    }
-  ],
-  "postsubmit": [
-    {
-      "name": "NearbyUnitTests"
     }
   ]
   // TODO(b/193602229): uncomment once it's supported.
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 73feee4..fc680d9 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -76,6 +76,19 @@
 }
 
 static Status initPrograms(const char* cg2_path) {
+    // This code was mainlined in T, so this should be trivially satisfied.
+    if (!modules::sdklevel::IsAtLeastT()) abort();
+
+    // S requires eBPF support which was only added in 4.9, so this should be satisfied.
+    if (!bpf::isAtLeastKernelVersion(4, 9, 0)) abort();
+
+    // U bumps the kernel requirement up to 4.14
+    if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) abort();
+
+    // V bumps the kernel requirement up to 4.19
+    if (modules::sdklevel::IsAtLeastV() && !bpf::isAtLeastKernelVersion(4, 19, 0)) abort();
+
+    // U mandates this mount point (though it should also be the case on T)
     if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
 
     unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
diff --git a/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
index f53e2dc..1414f7e 100644
--- a/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
+++ b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
@@ -16,10 +16,7 @@
 
 package android.remoteauth;
 
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-
 import android.annotation.NonNull;
-import android.annotation.SystemApi;
 
 import androidx.annotation.IntDef;
 
@@ -31,7 +28,7 @@
  *
  * @hide
  */
-@SystemApi(client = MODULE_LIBRARIES)
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
 public interface DeviceDiscoveryCallback {
     /** The device is no longer seen in the discovery process. */
     int STATE_LOST = 0;
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
index dfd7726..112ffa8 100644
--- a/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
@@ -16,7 +16,6 @@
 
 package android.remoteauth;
 
-import android.annotation.SystemApi;
 import android.app.SystemServiceRegistry;
 import android.content.Context;
 
@@ -25,7 +24,7 @@
  *
  * @hide
  */
-@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
 public final class RemoteAuthFrameworkInitializer {
     private RemoteAuthFrameworkInitializer() {}
 
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
index c025a55..038af2a 100644
--- a/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
@@ -16,14 +16,12 @@
 
 package android.remoteauth;
 
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 import static android.remoteauth.DeviceDiscoveryCallback.STATE_LOST;
 import static android.remoteauth.DeviceDiscoveryCallback.STATE_SEEN;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UserIdInt;
 import android.content.Context;
@@ -47,7 +45,7 @@
  *
  * @hide
  */
-@SystemApi(client = MODULE_LIBRARIES)
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
 // TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
 // is automerges from aosp-main to udc-mainline-prod
 @SystemService(RemoteAuthManager.REMOTE_AUTH_SERVICE)
@@ -79,7 +77,7 @@
      * @return true if this device can be enrolled
      * @hide
      */
-    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
     public boolean isRemoteAuthSupported() {
         try {
@@ -100,7 +98,7 @@
      * @return {@code true} if discovery began successfully, {@code false} otherwise
      * @hide
      */
-    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
     public boolean startDiscovery(
             int timeoutMs,
@@ -149,7 +147,7 @@
     // Suppressed lint: Registration methods should have overload that accepts delivery Executor.
     // Already have executor in startDiscovery() method.
     @SuppressLint("ExecutorRegistration")
-    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
     public void stopDiscovery(@NonNull DeviceDiscoveryCallback callback) {
         Preconditions.checkNotNull(callback, "invalid null scanCallback");
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.java b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
index 4cd2399..b6ede2e 100644
--- a/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
@@ -16,12 +16,9 @@
 
 package android.remoteauth;
 
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -35,7 +32,7 @@
  * @hide
  */
 // TODO(b/295407748) Change to use @DataClass
-@SystemApi(client = MODULE_LIBRARIES)
+// TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
 public final class RemoteDevice implements Parcelable {
     /** The remote device is not registered as remote authenticator. */
     public static final int STATE_NOT_REGISTERED = 0;
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index c3a9fb3..dfaf8cf 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -25,9 +25,10 @@
 java_library {
     name: "service-remoteauth-pre-jarjar",
     srcs: [":remoteauth-service-srcs"],
-
+    required: ["libremoteauth_jni_rust_defaults"],
     defaults: [
-        "framework-system-server-module-defaults"
+        "enable-remoteauth-targets",
+        "framework-system-server-module-defaults",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -39,6 +40,7 @@
         "framework-statsd",
     ],
     static_libs: [
+        "guava",
         "libprotobuf-java-lite",
         "fast-pair-lite-protos",
         "modules-utils-build",
@@ -46,6 +48,7 @@
         "modules-utils-preconditions",
         "modules-utils-backgroundthread",
         "presence-lite-protos",
+        "uwb_androidx_backend",
     ],
     sdk_version: "system_server_current",
     // This is included in service-connectivity which is 30+
@@ -69,7 +72,7 @@
     name: "statslog-remoteauth-java-gen",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module remoteauth " +
-         " --javaPackage com.android.server.remoteauth.proto --javaClass RemoteAuthStatsLog" +
-         " --minApiLevel 33",
+        " --javaPackage com.android.server.remoteauth.proto --javaClass RemoteAuthStatsLog" +
+        " --minApiLevel 33",
     out: ["com/android/server/remoteauth/proto/RemoteAuthStatsLog.java"],
 }
diff --git a/remoteauth/service/java/com/android/server/remoteauth/README.md b/remoteauth/service/java/com/android/server/remoteauth/README.md
index 2f8b096..b659bf7 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/README.md
+++ b/remoteauth/service/java/com/android/server/remoteauth/README.md
@@ -1,4 +1,10 @@
 This is the source root for the RemoteAuthService
 
-## Remote connectivity manager
+## Connectivity
 Provides the connectivity manager to manage connections with the peer device.
+
+## Ranging
+Provides the ranging manager to perform ranging with the peer devices.
+
+## Util
+Common utilities.
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
index 41ce89a..9374ace 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
@@ -27,6 +27,7 @@
 /** Service implementing remoteauth functionality. */
 public class RemoteAuthService extends IRemoteAuthService.Stub {
     public static final String TAG = "RemoteAuthService";
+    public static final String SERVICE_NAME = Context.REMOTE_AUTH_SERVICE;
 
     public RemoteAuthService(Context context) {
         Preconditions.checkNotNull(context);
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java
new file mode 100644
index 0000000..f79ec7e
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/INativeRemoteAuthService.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.jni;
+
+/**
+ * Interface defining a proxy between Rust and Java implementation of RemoteAuth protocol.
+ *
+ * @hide
+ */
+public interface INativeRemoteAuthService {
+    /**
+     * Interface for RemoteAuth PAL
+     *
+     * @hide
+     */
+    interface IPlatform {
+        /**
+         * Sends message to the remote authenticator
+         *
+         * @param connectionId connection ID of the {@link android.remoteauth.RemoteAuthenticator}
+         * @param request payload of the request
+         * @param callback to be used to pass the response result
+         *
+         * @hide
+         */
+        void sendRequest(int connectionId, byte[] request, ResponseCallback callback);
+
+        /**
+         * Interface for a callback to send a response back.
+         *
+         * @hide
+         */
+        interface ResponseCallback {
+            /**
+             * Invoked when message sending succeeds.
+             *
+             * @param response contains response
+             *
+             * @hide
+             */
+            void onSuccess(byte[] response);
+
+            /**
+             * Invoked when message sending fails.
+             *
+             * @param errorCode indicating the error
+             *
+             * @hide
+             */
+            void onFailure(int errorCode);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java
new file mode 100644
index 0000000..39c2a74
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/NativeRemoteAuthService.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.jni;
+
+import com.android.internal.annotations.Keep;
+import com.android.server.remoteauth.jni.INativeRemoteAuthService.IPlatform;
+
+/**
+ * A service providing a proxy between Rust implementation and {@link
+ * com.android.server.remoteauth.RemoteAuthService}.
+ *
+ * @hide
+ */
+public class NativeRemoteAuthService {
+    private static final String TAG = NativeRemoteAuthService.class.getSimpleName();
+
+    private IPlatform mPlatform;
+    public final Object mNativeLock = new Object();
+
+    // Constructor should receive pointers to:
+    // ConnectivityManager, RangingManager and DB
+    public NativeRemoteAuthService() {
+        System.loadLibrary("remoteauth_jni_rust");
+        synchronized (mNativeLock) {
+            native_init();
+        }
+    }
+
+    public void setDeviceListener(final IPlatform platform) {
+        mPlatform = platform;
+    }
+
+    /**
+     * Sends message to the remote authenticator
+     *
+     * @param connectionId connection ID of the {@link android.remoteauth.RemoteAuthenticator}
+     * @param request payload of the request
+     * @param responseHandle a handle associated with the request, used to pass the response to the
+     *     platform
+     * @param platformHandle a handle associated with the platform object, used to pass the response
+     *     to the specific platform
+     *
+     * @hide
+     */
+    @Keep
+    public void sendRequest(
+            int connectionId, byte[] request, long responseHandle, long platformHandle) {
+        mPlatform.sendRequest(
+                connectionId,
+                request,
+                new IPlatform.ResponseCallback() {
+                    @Override
+                    public void onSuccess(byte[] response) {
+                        synchronized (mNativeLock) {
+                            native_on_send_request_success(
+                                    response, platformHandle, responseHandle);
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(int errorCode) {
+                        synchronized (mNativeLock) {
+                            native_on_send_request_error(errorCode, platformHandle, responseHandle);
+                        }
+                    }
+                });
+    }
+
+    /* Native functions implemented in JNI */
+    // This function should be implemented in remoteauth_jni_android_protocol
+    private native boolean native_init();
+
+    private native void native_on_send_request_success(
+            byte[] appResponse, long platformHandle, long responseHandle);
+
+    private native void native_on_send_request_error(
+            int errorCode, long platformHandle, long responseHandle);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java b/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java
new file mode 100644
index 0000000..3ae9838
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/jni/PlatformBadHandleException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+/**
+ * Represents an unrecoverable error (invalid handle) that has occurred during accessing the
+ * platform.
+ */
+package com.android.server.remoteauth.jni;
+
+import com.android.internal.annotations.Keep;
+/**
+ * Exception thrown by native platform rust implementation of {@link
+ * com.android.server.remoteauth.RemoteAuthService}.
+ *
+ * @hide
+ */
+@Keep
+public class PlatformBadHandleException extends Exception {
+    public PlatformBadHandleException(final String message) {
+        super(message);
+    }
+
+    public PlatformBadHandleException(final Exception e) {
+        super(e);
+    }
+
+    public PlatformBadHandleException(final String message, final Exception e) {
+        super(message, e);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
new file mode 100644
index 0000000..36aa585
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import androidx.annotation.IntDef;
+
+import com.google.common.collect.ImmutableList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** The ranging capabilities of the device. */
+public class RangingCapabilities {
+
+    /** Possible ranging methods */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                RANGING_METHOD_UNKNOWN,
+                RANGING_METHOD_UWB,
+            })
+    public @interface RangingMethod {}
+
+    /** Unknown ranging method. */
+    public static final int RANGING_METHOD_UNKNOWN = 0x0;
+
+    /** Ultra-wideband ranging. */
+    public static final int RANGING_METHOD_UWB = 0x1;
+
+    private final ImmutableList<Integer> mSupportedRangingMethods;
+    private final androidx.core.uwb.backend.impl.internal.RangingCapabilities
+            mUwbRangingCapabilities;
+
+    /**
+     * Gets the list of supported ranging methods of the device.
+     *
+     * @return list of {@link RangingMethod}
+     */
+    public ImmutableList<Integer> getSupportedRangingMethods() {
+        return mSupportedRangingMethods;
+    }
+
+    /**
+     * Gets the UWB ranging capabilities of the device.
+     *
+     * @return UWB ranging capabilities, null if UWB is not a supported {@link RangingMethod} in
+     *     {@link #getSupportedRangingMethods}.
+     */
+    @Nullable
+    public androidx.core.uwb.backend.impl.internal.RangingCapabilities getUwbRangingCapabilities() {
+        return mUwbRangingCapabilities;
+    }
+
+    private RangingCapabilities(
+            List<Integer> supportedRangingMethods,
+            androidx.core.uwb.backend.impl.internal.RangingCapabilities uwbRangingCapabilities) {
+        mSupportedRangingMethods = ImmutableList.copyOf(supportedRangingMethods);
+        mUwbRangingCapabilities = uwbRangingCapabilities;
+    }
+
+    /** Builder class for {@link RangingCapabilities}. */
+    public static final class Builder {
+        private List<Integer> mSupportedRangingMethods = new ArrayList<>();
+        private androidx.core.uwb.backend.impl.internal.RangingCapabilities mUwbRangingCapabilities;
+
+        /** Adds a supported {@link RangingMethod} */
+        public Builder addSupportedRangingMethods(@RangingMethod int rangingMethod) {
+            mSupportedRangingMethods.add(rangingMethod);
+            return this;
+        }
+
+        /** Sets the uwb ranging capabilities. */
+        public Builder setUwbRangingCapabilities(
+                @NonNull
+                        androidx.core.uwb.backend.impl.internal.RangingCapabilities
+                                uwbRangingCapabilities) {
+            mUwbRangingCapabilities = uwbRangingCapabilities;
+            return this;
+        }
+
+        /** Builds {@link RangingCapabilities}. */
+        public RangingCapabilities build() {
+            return new RangingCapabilities(mSupportedRangingMethods, mUwbRangingCapabilities);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
new file mode 100644
index 0000000..cf0db76
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static android.content.pm.PackageManager.FEATURE_UWB;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.uwb.backend.impl.internal.UwbFeatureFlags;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Manages the creation of generic device to device ranging session and obtaining device's ranging
+ * capabilities.
+ *
+ * <p>Out-of-band channel for ranging capabilities/parameters exchange is assumed being handled
+ * outside of this class.
+ */
+public class RangingManager {
+    private static final String TAG = "RangingManager";
+
+    private Context mContext;
+    @NonNull private RangingCapabilities mCachedRangingCapabilities;
+    @NonNull private UwbServiceImpl mUwbServiceImpl;
+
+    public RangingManager(@NonNull Context context) {
+        mContext = context;
+        if (mContext.getPackageManager().hasSystemFeature(FEATURE_UWB)) {
+            initiateUwb();
+        }
+    }
+
+    /**
+     * Shutdown and stop all listeners and tasks. After shutdown, RangingManager shall not be used
+     * anymore.
+     */
+    public void shutdown() {
+        if (mUwbServiceImpl != null) {
+            mUwbServiceImpl.shutdown();
+        }
+        Log.i(TAG, "shutdown");
+    }
+
+    /**
+     * Gets the {@link RangingCapabilities} of this device.
+     *
+     * @return RangingCapabilities.
+     */
+    @NonNull
+    public RangingCapabilities getRangingCapabilities() {
+        if (mCachedRangingCapabilities == null) {
+            RangingCapabilities.Builder builder = new RangingCapabilities.Builder();
+            if (mUwbServiceImpl != null) {
+                builder.addSupportedRangingMethods(RANGING_METHOD_UWB);
+                builder.setUwbRangingCapabilities(mUwbServiceImpl.getRangingCapabilities());
+            }
+            mCachedRangingCapabilities = builder.build();
+        }
+        return mCachedRangingCapabilities;
+    }
+
+    /**
+     * Creates a {@link RangingSession} based on the given {@link SessionParameters}, which shall be
+     * provided based on the rangingCapabilities of the device.
+     *
+     * @param sessionParameters parameters used to setup the session.
+     * @return the created RangingSession. Null if session creation failed.
+     * @throws IllegalArgumentException if sessionParameters is invalid.
+     */
+    @Nullable
+    public RangingSession createSession(@NonNull SessionParameters sessionParameters) {
+        Preconditions.checkNotNull(sessionParameters, "sessionParameters must not be null");
+        switch (sessionParameters.getRangingMethod()) {
+            case RANGING_METHOD_UWB:
+                if (mUwbServiceImpl == null) {
+                    Log.w(TAG, "createSession with UWB failed - UWB not supported");
+                    break;
+                }
+                return new UwbRangingSession(mContext, sessionParameters, mUwbServiceImpl);
+            case RANGING_METHOD_UNKNOWN:
+                break;
+        }
+        return null;
+    }
+
+    /** Initiation required for ranging with UWB. */
+    private void initiateUwb() {
+        UwbFeatureFlags uwbFeatureFlags =
+                new UwbFeatureFlags.Builder()
+                        .setSkipRangingCapabilitiesCheck(
+                                Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
+                        .setReversedByteOrderFiraParams(
+                                Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU)
+                        .build();
+        mUwbServiceImpl = new UwbServiceImpl(mContext, uwbFeatureFlags);
+        Log.i(TAG, "RangingManager initiateUwb complete");
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
new file mode 100644
index 0000000..923730c
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+/** The set of parameters to start ranging. */
+public class RangingParameters {}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java
new file mode 100644
index 0000000..5e582b1
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingReport.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import androidx.annotation.IntDef;
+
+/** Holds ranging report data. */
+public class RangingReport {
+
+    /**
+     * State of the proximity based on detected distance compared against specified near and far
+     * boundaries.
+     */
+    @IntDef(
+            value = {
+                PROXIMITY_STATE_UNKNOWN,
+                PROXIMITY_STATE_INSIDE,
+                PROXIMITY_STATE_OUTSIDE,
+            })
+    public @interface ProximityState {}
+
+    /** Unknown proximity state. */
+    public static final int PROXIMITY_STATE_UNKNOWN = 0x0;
+
+    /**
+     * Proximity is inside the lower and upper proximity boundary. lowerProximityBoundaryM <=
+     * proximity <= upperProximityBoundaryM
+     */
+    public static final int PROXIMITY_STATE_INSIDE = 0x1;
+
+    /**
+     * Proximity is outside the lower and upper proximity boundary. proximity <
+     * lowerProximityBoundaryM OR upperProximityBoundaryM < proximity
+     */
+    public static final int PROXIMITY_STATE_OUTSIDE = 0x2;
+
+    private final float mDistanceM;
+    @ProximityState private final int mProximityState;
+
+    /**
+     * Gets the distance measurement in meters.
+     *
+     * <p>Value may be negative for devices in very close proximity.
+     *
+     * @return distance in meters
+     */
+    public float getDistanceM() {
+        return mDistanceM;
+    }
+
+    /**
+     * Gets the {@link ProximityState}.
+     *
+     * <p>The state is computed based on {@link #getDistanceM} and proximity related session
+     * parameters.
+     *
+     * @return proximity state
+     */
+    @ProximityState
+    public int getProximityState() {
+        return mProximityState;
+    }
+
+    private RangingReport(float distanceM, @ProximityState int proximityState) {
+        mDistanceM = distanceM;
+        mProximityState = proximityState;
+    }
+
+    /** Builder class for {@link RangingReport}. */
+    public static final class Builder {
+        private float mDistanceM;
+        @ProximityState private int mProximityState;
+
+        /** Sets the distance in meters. */
+        public Builder setDistanceM(float distanceM) {
+            mDistanceM = distanceM;
+            return this;
+        }
+
+        /** Sets the proximity state. */
+        public Builder setProximityState(@ProximityState int proximityState) {
+            mProximityState = proximityState;
+            return this;
+        }
+
+        /** Builds {@link RangingReport}. */
+        public RangingReport build() {
+            return new RangingReport(mDistanceM, mProximityState);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
new file mode 100644
index 0000000..adb36c5
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.util.Crypto;
+
+import com.google.common.hash.Hashing;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * The controller for starting and stopping ranging during which callers receive callbacks with
+ * {@link RangingReport}s and {@link RangingError}s."
+ *
+ * <p>A session can be started and stopped multiple times. After starting, updates ({@link
+ * RangingReport}, {@link RangingError}, etc) will be reported via the provided {@link
+ * RangingCallback}. BaseKey and SyncData are used for auto derivation of supported ranging
+ * parameters, which will be implementation specific.
+ *
+ * <p>Ranging method specific implementation shall be implemented in the extended class.
+ */
+public abstract class RangingSession {
+    private static final String TAG = "RangingSession";
+
+    /** Types of ranging error. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                RANGING_ERROR_UNKNOWN,
+                RANGING_ERROR_INVALID_PARAMETERS,
+                RANGING_ERROR_STOPPED_BY_REQUEST,
+                RANGING_ERROR_STOPPED_BY_PEER,
+                RANGING_ERROR_FAILED_TO_START,
+                RANGING_ERROR_FAILED_TO_STOP,
+                RANGING_ERROR_SYSTEM_ERROR,
+                RANGING_ERROR_SYSTEM_TIMEOUT,
+            })
+    public @interface RangingError {}
+
+    /** Unknown ranging error type. */
+    public static final int RANGING_ERROR_UNKNOWN = 0x0;
+
+    /** Ranging error due to invalid parameters. */
+    public static final int RANGING_ERROR_INVALID_PARAMETERS = 0x1;
+
+    /** Ranging error due to stopped by calling {@link #stop}. */
+    public static final int RANGING_ERROR_STOPPED_BY_REQUEST = 0x2;
+
+    /** Ranging error due to stopped by the peer device. */
+    public static final int RANGING_ERROR_STOPPED_BY_PEER = 0x3;
+
+    /** Ranging error due to failure to start ranging. */
+    public static final int RANGING_ERROR_FAILED_TO_START = 0x4;
+
+    /** Ranging error due to failure to stop ranging. */
+    public static final int RANGING_ERROR_FAILED_TO_STOP = 0x5;
+
+    /**
+     * Ranging error due to system error cause by changes such as privacy policy, power management
+     * policy, permissions, and more.
+     */
+    public static final int RANGING_ERROR_SYSTEM_ERROR = 0x6;
+
+    /** Ranging error due to system timeout in retry attempts. */
+    public static final int RANGING_ERROR_SYSTEM_TIMEOUT = 0x7;
+
+    /** Interface for ranging update callbacks. */
+    public interface RangingCallback {
+        /**
+         * Call upon new {@link RangingReport}.
+         *
+         * @param sessionInfo info about this ranging session.
+         * @param rangingReport new ranging report
+         */
+        void onRangingReport(
+                @NonNull SessionInfo sessionInfo, @NonNull RangingReport rangingReport);
+
+        /**
+         * Call upon any ranging error events.
+         *
+         * @param sessionInfo info about this ranging session.
+         * @param rangingError error type
+         */
+        void onError(@NonNull SessionInfo sessionInfo, @RangingError int rangingError);
+    }
+
+    protected Context mContext;
+    protected SessionInfo mSessionInfo;
+    protected float mLowerProximityBoundaryM;
+    protected float mUpperProximityBoundaryM;
+    protected boolean mAutoDeriveParams;
+    protected byte[] mBaseKey;
+    protected byte[] mSyncData;
+    protected int mSyncCounter;
+    protected byte[] mDerivedData;
+    protected int mDerivedDataLength;
+
+    protected RangingSession(
+            @NonNull Context context,
+            @NonNull SessionParameters sessionParameters,
+            int derivedDataLength) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(sessionParameters);
+        mContext = context;
+        mSessionInfo =
+                new SessionInfo.Builder()
+                        .setDeviceId(sessionParameters.getDeviceId())
+                        .setRangingMethod(sessionParameters.getRangingMethod())
+                        .build();
+        mLowerProximityBoundaryM = sessionParameters.getLowerProximityBoundaryM();
+        mUpperProximityBoundaryM = sessionParameters.getUpperProximityBoundaryM();
+        mAutoDeriveParams = sessionParameters.getAutoDeriveParams();
+        Log.i(
+                TAG,
+                "Creating a new RangingSession {info = "
+                        + mSessionInfo
+                        + ", autoDeriveParams = "
+                        + mAutoDeriveParams
+                        + "}");
+        if (mAutoDeriveParams) {
+            Preconditions.checkArgument(
+                    derivedDataLength > 0, "derivedDataLength must be greater than 0");
+            mDerivedDataLength = derivedDataLength;
+            resetBaseKey(sessionParameters.getBaseKey());
+            resetSyncData(sessionParameters.getSyncData());
+        }
+    }
+
+    /**
+     * Starts ranging based on the given {@link RangingParameters}.
+     *
+     * <p>Start can be called again after {@link #stop()} has been called, else it will result in a
+     * no-op.
+     *
+     * @param rangingParameters parameters to start the ranging.
+     * @param executor Executor to run the rangingCallback.
+     * @param rangingCallback callback to notify of ranging events.
+     * @throws NullPointerException if params are null.
+     * @throws IllegalArgumentException if rangingParameters is invalid.
+     */
+    public abstract void start(
+            @NonNull RangingParameters rangingParameters,
+            @NonNull Executor executor,
+            @NonNull RangingCallback rangingCallback);
+
+    /**
+     * Stops ranging.
+     *
+     * <p>Calling stop without first calling {@link #start()} will result in a no-op.
+     */
+    public abstract void stop();
+
+    /**
+     * Resets the base key that's used to derive all possible ranging parameters. The baseKey shall
+     * be reset whenever there is a risk that it may no longer be valid and secured. For example,
+     * the secure connection between the devices is lost.
+     *
+     * @param baseKey new baseKey must be 16 or 32 bytes.
+     * @throws NullPointerException if baseKey is null.
+     * @throws IllegalArgumentException if baseKey has invalid length.
+     */
+    public void resetBaseKey(@NonNull byte[] baseKey) {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, new baseKey is ignored.");
+            return;
+        }
+        Preconditions.checkNotNull(baseKey);
+        if (baseKey.length != 16 && baseKey.length != 32) {
+            throw new IllegalArgumentException("Invalid baseKey length: " + baseKey.length);
+        }
+        mBaseKey = baseKey;
+        updateDerivedData();
+        Log.i(TAG, "resetBaseKey");
+    }
+
+    /**
+     * Resets the synchronization by giving a new syncData used for ranging parameters derivation.
+     * Resetting the syncData is not required before each {@link #start}, but the more time the
+     * derivations are done before resetting syncData, the higher the risk the derivation will be
+     * out of sync between the devices. Therefore, syncData shall be refreshed in a best effort
+     * manner.
+     *
+     * @param syncData new syncData must be 16 bytes.
+     * @throws NullPointerException if baseKey is null.
+     * @throws IllegalArgumentException if syncData has invalid length.
+     */
+    public void resetSyncData(@NonNull byte[] syncData) {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, new syncData is ignored.");
+            return;
+        }
+        Preconditions.checkNotNull(syncData);
+        if (syncData.length != 16) {
+            throw new IllegalArgumentException("Invalid syncData length: " + syncData.length);
+        }
+        mSyncData = syncData;
+        mSyncCounter = 0;
+        updateDerivedData();
+        Log.i(TAG, "resetSyncData");
+    }
+
+    /** Recomputes mDerivedData using the latest mBaseKey, mSyncData, and mSyncCounter. */
+    protected boolean updateDerivedData() {
+        if (!mAutoDeriveParams) {
+            Log.w(TAG, "autoDeriveParams is disabled, updateDerivedData is skipped.");
+            return false;
+        }
+        if (mBaseKey == null
+                || mBaseKey.length == 0
+                || mSyncData == null
+                || mSyncData.length == 0) {
+            Log.w(TAG, "updateDerivedData: Missing baseKey/syncData");
+            return false;
+        }
+        byte[] hashedSyncData =
+                Hashing.sha256()
+                        .newHasher()
+                        .putBytes(mSyncData)
+                        .putInt(mSyncCounter)
+                        .hash()
+                        .asBytes();
+        byte[] newDerivedData = Crypto.computeHkdf(mBaseKey, hashedSyncData, mDerivedDataLength);
+        if (newDerivedData == null) {
+            Log.w(TAG, "updateDerivedData: computeHkdf failed");
+            return false;
+        }
+        mDerivedData = newDerivedData;
+        mSyncCounter++;
+        Log.i(TAG, "updateDerivedData");
+        return true;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
new file mode 100644
index 0000000..0ec640c
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+/** Information about the {@link RangingSession}. */
+public class SessionInfo {
+
+    private final String mDeviceId;
+    @RangingMethod private final int mRangingMethod;
+
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    @RangingMethod
+    public int getRangingMethod() {
+        return mRangingMethod;
+    }
+
+    private SessionInfo(String deviceId, @RangingMethod int rangingMethod) {
+        mDeviceId = deviceId;
+        mRangingMethod = rangingMethod;
+    }
+
+    @Override
+    public String toString() {
+        return "SessionInfo { "
+                + "DeviceId = "
+                + mDeviceId
+                + "RangingMethod = "
+                + mRangingMethod
+                + " }";
+    }
+
+    /** Builder class for {@link SessionInfo}. */
+    public static final class Builder {
+        private String mDeviceId = "";
+        @RangingMethod private int mRangingMethod = RANGING_METHOD_UNKNOWN;
+
+        /** Sets the device id. */
+        public Builder setDeviceId(String deviceId) {
+            mDeviceId = deviceId;
+            return this;
+        }
+
+        /** Sets the ranging method. */
+        public Builder setRangingMethod(@RangingMethod int rangingMethod) {
+            mRangingMethod = rangingMethod;
+            return this;
+        }
+
+        /** Builds {@link SessionInfo}. */
+        public SessionInfo build() {
+            Preconditions.checkArgument(!mDeviceId.isEmpty(), "deviceId must not be empty.");
+            Preconditions.checkArgument(
+                    mRangingMethod != RANGING_METHOD_UNKNOWN, "Unknown rangingMethod");
+            return new SessionInfo(mDeviceId, mRangingMethod);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
new file mode 100644
index 0000000..2f71244
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UNKNOWN;
+
+import android.annotation.NonNull;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The set of parameters to create a ranging session.
+ *
+ * <p>Required parameters must be provided, else {@link Builder} will throw an exception. The
+ * optional parameters only need to be provided if the functionality is necessary to the session,
+ * see the setter functions of the {@link Builder} for detailed info of each parameter.
+ */
+public class SessionParameters {
+
+    /** Ranging device role. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                DEVICE_ROLE_UNKNOWN,
+                DEVICE_ROLE_INITIATOR,
+                DEVICE_ROLE_RESPONDER,
+            })
+    public @interface DeviceRole {}
+
+    /** Unknown device role. */
+    public static final int DEVICE_ROLE_UNKNOWN = 0x0;
+
+    /** Device that initiates the ranging. */
+    public static final int DEVICE_ROLE_INITIATOR = 0x1;
+
+    /** Device that responds to ranging. */
+    public static final int DEVICE_ROLE_RESPONDER = 0x2;
+
+    /* Required parameters */
+    private final String mDeviceId;
+    @RangingMethod private final int mRangingMethod;
+    @DeviceRole private final int mDeviceRole;
+
+    /* Optional parameters */
+    private final float mLowerProximityBoundaryM;
+    private final float mUpperProximityBoundaryM;
+    private final boolean mAutoDeriveParams;
+    private final byte[] mBaseKey;
+    private final byte[] mSyncData;
+
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    @RangingMethod
+    public int getRangingMethod() {
+        return mRangingMethod;
+    }
+
+    @DeviceRole
+    public int getDeviceRole() {
+        return mDeviceRole;
+    }
+
+    public float getLowerProximityBoundaryM() {
+        return mLowerProximityBoundaryM;
+    }
+
+    public float getUpperProximityBoundaryM() {
+        return mUpperProximityBoundaryM;
+    }
+
+    public boolean getAutoDeriveParams() {
+        return mAutoDeriveParams;
+    }
+
+    public byte[] getBaseKey() {
+        return mBaseKey;
+    }
+
+    public byte[] getSyncData() {
+        return mSyncData;
+    }
+
+    private SessionParameters(
+            String deviceId,
+            @RangingMethod int rangingMethod,
+            @DeviceRole int deviceRole,
+            float lowerProximityBoundaryM,
+            float upperProximityBoundaryM,
+            boolean autoDeriveParams,
+            byte[] baseKey,
+            byte[] syncData) {
+        mDeviceId = deviceId;
+        mRangingMethod = rangingMethod;
+        mDeviceRole = deviceRole;
+        mLowerProximityBoundaryM = lowerProximityBoundaryM;
+        mUpperProximityBoundaryM = upperProximityBoundaryM;
+        mAutoDeriveParams = autoDeriveParams;
+        mBaseKey = baseKey;
+        mSyncData = syncData;
+    }
+
+    /** Builder class for {@link SessionParameters}. */
+    public static final class Builder {
+        private String mDeviceId = new String("");
+        @RangingMethod private int mRangingMethod = RANGING_METHOD_UNKNOWN;
+        @DeviceRole private int mDeviceRole = DEVICE_ROLE_UNKNOWN;
+        private float mLowerProximityBoundaryM;
+        private float mUpperProximityBoundaryM;
+        private boolean mAutoDeriveParams = false;
+        private byte[] mBaseKey = new byte[] {};
+        private byte[] mSyncData = new byte[] {};
+
+        /**
+         * Sets the device id.
+         *
+         * <p>This is used as the identity included in the {@link SessionInfo} for all {@link
+         * RangingCallback}s.
+         */
+        public Builder setDeviceId(@NonNull String deviceId) {
+            mDeviceId = deviceId;
+            return this;
+        }
+
+        /**
+         * Sets the {@link RangingMethod} to be used for the {@link RangingSession}.
+         *
+         * <p>Note: The ranging method should be ones in the list return by {@link
+         * RangingCapabilities#getSupportedRangingMethods};
+         */
+        public Builder setRangingMethod(@RangingMethod int rangingMethod) {
+            mRangingMethod = rangingMethod;
+            return this;
+        }
+
+        /** Sets the {@link DeviceRole} to be used for the {@link RangingSession}. */
+        public Builder setDeviceRole(@DeviceRole int deviceRole) {
+            mDeviceRole = deviceRole;
+            return this;
+        }
+
+        /**
+         * Sets the lower proximity boundary in meters, must be greater than or equals to zero.
+         *
+         * <p>This value is used to compute the {@link ProximityState} = {@link
+         * PROXIMITY_STATE_INSIDE} if lowerProximityBoundaryM <= proximity <=
+         * upperProximityBoundaryM, else {@link PROXIMITY_STATE_OUTSIDE}.
+         */
+        public Builder setLowerProximityBoundaryM(float lowerProximityBoundaryM) {
+            mLowerProximityBoundaryM = lowerProximityBoundaryM;
+            return this;
+        }
+
+        /**
+         * Sets the upper proximity boundary in meters, must be greater than or equals to
+         * lowerProximityBoundaryM.
+         *
+         * <p>This value is used to compute the {@link ProximityState} = {@link
+         * PROXIMITY_STATE_INSIDE} if lowerProximityBoundaryM <= proximity <=
+         * upperProximityBoundaryM, else {@link PROXIMITY_STATE_OUTSIDE}.
+         */
+        public Builder setUpperProximityBoundaryM(float upperProximityBoundaryM) {
+            mUpperProximityBoundaryM = upperProximityBoundaryM;
+            return this;
+        }
+
+        /**
+         * Sets the auto derive ranging parameters flag. Defaults to false.
+         *
+         * <p>This enables the {@link RangingSession} to automatically derive all possible {@link
+         * RangingParameters} at each {@link RangingSession#start} using the provided {@link
+         * #setBaseKey} and {@link #setSyncData}, which shall be securely shared between the ranging
+         * devices out of band.
+         */
+        public Builder setAutoDeriveParams(boolean autoDeriveParams) {
+            mAutoDeriveParams = autoDeriveParams;
+            return this;
+        }
+
+        /**
+         * Sets the base key. Only required if {@link #setAutoDeriveParams} is set to true.
+         *
+         * @param baseKey baseKey must be 16 or 32 bytes.
+         * @throws NullPointerException if baseKey is null
+         */
+        public Builder setBaseKey(@NonNull byte[] baseKey) {
+            Preconditions.checkNotNull(baseKey);
+            mBaseKey = baseKey;
+            return this;
+        }
+
+        /**
+         * Sets the sync data. Only required if {@link #setAutoDeriveParams} is set to true.
+         *
+         * @param syncData syncData must be 16 bytes.
+         * @throws NullPointerException if syncData is null
+         */
+        public Builder setSyncData(@NonNull byte[] syncData) {
+            Preconditions.checkNotNull(syncData);
+            mSyncData = syncData;
+            return this;
+        }
+
+        /**
+         * Builds {@link SessionParameters}.
+         *
+         * @throws IllegalArgumentException if any parameter is invalid.
+         */
+        public SessionParameters build() {
+            Preconditions.checkArgument(!mDeviceId.isEmpty(), "deviceId must not be empty.");
+            Preconditions.checkArgument(
+                    mRangingMethod != RANGING_METHOD_UNKNOWN, "Unknown rangingMethod");
+            Preconditions.checkArgument(mDeviceRole != DEVICE_ROLE_UNKNOWN, "Unknown deviceRole");
+            Preconditions.checkArgument(
+                    mLowerProximityBoundaryM >= 0,
+                    "Negative lowerProximityBoundaryM: " + mLowerProximityBoundaryM);
+            Preconditions.checkArgument(
+                    mLowerProximityBoundaryM <= mUpperProximityBoundaryM,
+                    "lowerProximityBoundaryM is greater than upperProximityBoundaryM: "
+                            + mLowerProximityBoundaryM
+                            + " > "
+                            + mUpperProximityBoundaryM);
+            // If mAutoDeriveParams is false, mBaseKey and mSyncData will not be used.
+            if (mAutoDeriveParams) {
+                Preconditions.checkArgument(
+                        mBaseKey.length == 16 || mBaseKey.length == 32,
+                        "Invalid baseKey length: " + mBaseKey.length);
+                Preconditions.checkArgument(
+                        mSyncData.length == 16, "Invalid syncData length: " + mSyncData.length);
+            }
+
+            return new SessionParameters(
+                    mDeviceId,
+                    mRangingMethod,
+                    mDeviceRole,
+                    mLowerProximityBoundaryM,
+                    mUpperProximityBoundaryM,
+                    mAutoDeriveParams,
+                    mBaseKey,
+                    mSyncData);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
new file mode 100644
index 0000000..2015b66
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+
+import java.util.concurrent.Executor;
+
+/** UWB (ultra wide-band) implementation of {@link RangingSession}. */
+public class UwbRangingSession extends RangingSession {
+    private static final int DERIVED_DATA_LENGTH = 1;
+
+    public UwbRangingSession(
+            @NonNull Context context,
+            @NonNull SessionParameters sessionParameters,
+            @NonNull UwbServiceImpl uwbServiceImpl) {
+        super(context, sessionParameters, DERIVED_DATA_LENGTH);
+    }
+
+    @Override
+    public void start(
+            @NonNull RangingParameters rangingParameters,
+            @NonNull Executor executor,
+            @NonNull RangingCallback rangingCallback) {}
+
+    @Override
+    public void stop() {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java b/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java
new file mode 100644
index 0000000..573597f
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/util/Crypto.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.util;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Utility class of cryptographic functions. */
+public final class Crypto {
+    private static final String TAG = "Crypto";
+    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+    /**
+     * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+     * given size.
+     *
+     * @param ikm the input keying material.
+     * @param salt A possibly non-secret random value.
+     * @param size The length of the generated pseudorandom string in bytes. The maximal size is
+     *     255.DigestSize, where DigestSize is the size of the underlying HMAC.
+     * @return size pseudorandom bytes, null if failed.
+     */
+    // Based on
+    // google3/third_party/tink/java_src/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    @Nullable
+    public static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+        Mac mac;
+        try {
+            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+            return null;
+        }
+
+        if (size > 255 * mac.getMacLength()) {
+            Log.w(TAG, "Size too large. " + size + " > " + 255 * mac.getMacLength());
+            return null;
+        }
+
+        if (ikm == null || ikm.length == 0) {
+            Log.w(TAG, "Ikm cannot be empty.");
+            return null;
+        }
+
+        if (salt == null || salt.length == 0) {
+            Log.w(TAG, "Salt cannot be empty.");
+            return null;
+        }
+
+        try {
+            mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        try {
+            mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] digest = new byte[0];
+        int ctr = 1;
+        int pos = 0;
+        while (true) {
+            mac.update(digest);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
new file mode 100644
index 0000000..e6e8a43
--- /dev/null
+++ b/remoteauth/service/jni/Android.bp
@@ -0,0 +1,76 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libremoteauth_jni_rust_defaults",
+    crate_name: "remoteauth_jni_rust",
+    lints: "android",
+    clippy_lints: "android",
+    min_sdk_version: "35",
+    srcs: ["src/lib.rs"],
+    rustlibs: [
+        "libbinder_rs",
+        "libjni",
+        "liblazy_static",
+        "liblog_rust",
+        "liblogger",
+        "libnum_traits",
+        "libthiserror",
+        "libtokio",
+        "libanyhow",
+    ],
+    proc_macros: [
+        "libasync_trait",
+    ],
+    prefer_rlib: true,
+    apex_available: [
+        "com.android.remoteauth",
+    ],
+    host_supported: true,
+}
+
+rust_test {
+    name: "libremoteauth_jni_rust_tests",
+    defaults: ["libremoteauth_jni_rust_defaults"],
+    rustlibs: [
+    ],
+    target: {
+        android: {
+            test_suites: [
+                "general-tests",
+            ],
+            test_config_template: "remoteauth_rust_test_config_template.xml",
+        },
+        host: {
+            test_suites: [
+                "general-tests",
+            ],
+            data_libs: [
+                "libandroid_runtime_lazy",
+                "libbase",
+                "libbinder",
+                "libbinder_ndk",
+                "libcutils",
+                "liblog",
+                "libutils",
+            ],
+        },
+    },
+    test_options: {
+        unit_test: true,
+    },
+    // Support multilib variants (using different suffix per sub-architecture), which is needed on
+    // build targets with secondary architectures, as the MTS test suite packaging logic flattens
+    // all test artifacts into a single `testcases` directory.
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+    auto_gen_config: true,
+}
diff --git a/remoteauth/service/jni/remoteauth_rust_test_config_template.xml b/remoteauth/service/jni/remoteauth_rust_test_config_template.xml
new file mode 100644
index 0000000..673b451
--- /dev/null
+++ b/remoteauth/service/jni/remoteauth_rust_test_config_template.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<configuration description="Configuration for {MODULE} Rust tests">
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+    </test>
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.remoteauth" />
+    </object>
+</configuration>
\ No newline at end of file
diff --git a/remoteauth/service/jni/src/jnames.rs b/remoteauth/service/jni/src/jnames.rs
new file mode 100644
index 0000000..d7cc908
--- /dev/null
+++ b/remoteauth/service/jni/src/jnames.rs
@@ -0,0 +1,17 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+
+//! Name of java classes and methods for RemoteAuth platform:
+pub(crate) const SEND_REQUEST_MNAME: &str = "sendRequest";
+pub(crate) const SEND_REQUEST_MSIG: &str = "(I[BII)V";
diff --git a/remoteauth/service/jni/src/lib.rs b/remoteauth/service/jni/src/lib.rs
new file mode 100644
index 0000000..a816c94
--- /dev/null
+++ b/remoteauth/service/jni/src/lib.rs
@@ -0,0 +1,25 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+
+//! New rust RemoteAuth JNI library.
+//!
+//! This library takes the JNI calls from RemoteAuthService to the remoteauth protocol library
+//! and from protocol library to platform (Java interface)
+
+mod jnames;
+mod unique_jvm;
+mod utils;
+
+pub mod remoteauth_jni_android_platform;
+pub mod remoteauth_jni_android_protocol;
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
new file mode 100644
index 0000000..1967c1a
--- /dev/null
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -0,0 +1,293 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+
+use crate::jnames::{SEND_REQUEST_MNAME, SEND_REQUEST_MSIG};
+use crate::unique_jvm;
+use anyhow::anyhow;
+use jni::errors::Error as JNIError;
+use jni::objects::{GlobalRef, JMethodID, JObject, JValue};
+use jni::signature::TypeSignature;
+use jni::sys::{jbyteArray, jint, jlong, jvalue};
+use jni::{JNIEnv, JavaVM};
+use lazy_static::lazy_static;
+use log::{debug, error, info};
+use std::collections::HashMap;
+use std::sync::{
+    atomic::{AtomicI64, Ordering},
+    Arc, Mutex,
+};
+
+/// Macro capturing the name of the function calling this macro.
+///
+/// function_name()! -> &'static str
+/// Returns the function name as 'static reference.
+macro_rules! function_name {
+    () => {{
+        // Declares function f inside current function.
+        fn f() {}
+        fn type_name_of<T>(_: T) -> &'static str {
+            std::any::type_name::<T>()
+        }
+        // type name of f is struct_or_crate_name::calling_function_name::f
+        let name = type_name_of(f);
+        // Find and cut the rest of the path:
+        // Third to last character, up to the first semicolon: is calling_function_name
+        match &name[..name.len() - 3].rfind(':') {
+            Some(pos) => &name[pos + 1..name.len() - 3],
+            None => &name[..name.len() - 3],
+        }
+    }};
+}
+
+lazy_static! {
+    static ref HANDLE_MAPPING: Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>> =
+        Mutex::new(HashMap::new());
+    static ref HANDLE_RN: AtomicI64 = AtomicI64::new(0);
+}
+
+fn generate_platform_handle() -> i64 {
+    HANDLE_RN.fetch_add(1, Ordering::SeqCst)
+}
+
+fn insert_platform_handle(handle: i64, item: Arc<Mutex<JavaPlatform>>) {
+    if 0 == handle {
+        // Init once
+        logger::init(
+            logger::Config::default()
+                .with_tag_on_device("remoteauth")
+                .with_min_level(log::Level::Trace)
+                .with_filter("trace,jni=info"),
+        );
+    }
+    HANDLE_MAPPING.lock().unwrap().insert(handle, Arc::clone(&item));
+}
+
+pub trait ResponseCallback {
+    fn on_response(&mut self, response: Vec<u8>);
+    fn on_error(&mut self, error_code: i32);
+}
+
+pub trait Platform {
+    /// Send a binary message to the remote with the given connection id and return the response.
+    fn send_request(
+        &mut self,
+        connection_id: i32,
+        request: &[u8],
+        callback: Box<dyn ResponseCallback + Send>,
+    ) -> anyhow::Result<()>;
+}
+//////////////////////////////////
+
+pub struct JavaPlatform {
+    platform_handle: i64,
+    vm: &'static Arc<JavaVM>,
+    platform_native_obj: GlobalRef,
+    send_request_method_id: JMethodID,
+    map_futures: Mutex<HashMap<i64, Box<dyn ResponseCallback + Send>>>,
+    atomic_handle: AtomicI64,
+}
+
+impl JavaPlatform {
+    // Method to create JavaPlatform
+    pub fn create(
+        java_platform_native: JObject<'_>,
+    ) -> Result<Arc<Mutex<impl Platform>>, JNIError> {
+        let platform_handle = generate_platform_handle();
+        let platform = Arc::new(Mutex::new(JavaPlatform::new(
+            platform_handle,
+            unique_jvm::get_static_ref().ok_or(JNIError::InvalidCtorReturn)?,
+            java_platform_native,
+        )?));
+        insert_platform_handle(platform_handle, Arc::clone(&platform));
+        Ok(Arc::clone(&platform))
+    }
+
+    fn new(
+        platform_handle: i64,
+        vm: &'static Arc<JavaVM>,
+        java_platform_native: JObject,
+    ) -> Result<JavaPlatform, JNIError> {
+        vm.attach_current_thread().and_then(|env| {
+            let platform_class = env.get_object_class(java_platform_native)?;
+            let platform_native_obj = env.new_global_ref(java_platform_native)?;
+            let send_request_method: JMethodID =
+                env.get_method_id(platform_class, SEND_REQUEST_MNAME, SEND_REQUEST_MSIG)?;
+
+            Ok(Self {
+                platform_handle,
+                vm,
+                platform_native_obj,
+                send_request_method_id: send_request_method,
+                map_futures: Mutex::new(HashMap::new()),
+                atomic_handle: AtomicI64::new(0),
+            })
+        })
+    }
+}
+
+impl Platform for JavaPlatform {
+    fn send_request(
+        &mut self,
+        connection_id: i32,
+        request: &[u8],
+        callback: Box<dyn ResponseCallback + Send>,
+    ) -> anyhow::Result<()> {
+        let type_signature = TypeSignature::from_str(SEND_REQUEST_MSIG)
+            .map_err(|e| anyhow!("JNI: Invalid type signature: {:?}", e))?;
+
+        let response_handle = self.atomic_handle.fetch_add(1, Ordering::SeqCst);
+        self.map_futures.lock().unwrap().insert(response_handle, callback);
+        self.vm
+            .attach_current_thread()
+            .and_then(|env| {
+                let request_jbytearray = env.byte_array_from_slice(request)?;
+                // Safety: request_jbytearray is safely instantiated above.
+                let request_jobject = unsafe { JObject::from_raw(request_jbytearray) };
+
+                let _ = env.call_method_unchecked(
+                    self.platform_native_obj.as_obj(),
+                    self.send_request_method_id,
+                    type_signature.ret,
+                    &[
+                        jvalue::from(JValue::Int(connection_id)),
+                        jvalue::from(JValue::Object(request_jobject)),
+                        jvalue::from(JValue::Long(response_handle)),
+                        jvalue::from(JValue::Long(self.platform_handle)),
+                    ],
+                );
+                Ok(info!(
+                    "{} successfully sent-message, waiting for response {}:{}",
+                    function_name!(),
+                    self.platform_handle,
+                    response_handle
+                ))
+            })
+            .map_err(|e| anyhow!("JNI: Failed to attach current thread: {:?}", e))?;
+        Ok(())
+    }
+}
+
+impl JavaPlatform {
+    fn on_send_request_success(&mut self, response: &[u8], response_handle: i64) {
+        info!(
+            "{} completed successfully {}:{}",
+            function_name!(),
+            self.platform_handle,
+            response_handle
+        );
+        if let Some(mut callback) = self.map_futures.lock().unwrap().remove(&response_handle) {
+            callback.on_response(response.to_vec());
+        } else {
+            error!(
+                "Failed to find TX for {} and {}:{}",
+                function_name!(),
+                self.platform_handle,
+                response_handle
+            );
+        }
+    }
+
+    fn on_send_request_error(&self, error_code: i32, response_handle: i64) {
+        error!(
+            "{} completed with error {} {}:{}",
+            function_name!(),
+            error_code,
+            self.platform_handle,
+            response_handle
+        );
+        if let Some(mut callback) = self.map_futures.lock().unwrap().remove(&response_handle) {
+            callback.on_error(error_code);
+        } else {
+            error!(
+                "Failed to find callback for {} and {}:{}",
+                function_name!(),
+                self.platform_handle,
+                response_handle
+            );
+        }
+    }
+}
+
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_on_send_request_success(
+    env: JNIEnv,
+    _: JObject,
+    app_response: jbyteArray,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    debug!("{}: enter", function_name!());
+    native_on_send_request_success(env, app_response, platform_handle, response_handle);
+}
+
+fn native_on_send_request_success(
+    env: JNIEnv<'_>,
+    app_response: jbyteArray,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    if let Some(platform) = HANDLE_MAPPING.lock().unwrap().get(&platform_handle) {
+        let response =
+            env.convert_byte_array(app_response).map_err(|_| JNIError::InvalidCtorReturn).unwrap();
+        let mut platform = (*platform).lock().unwrap();
+        platform.on_send_request_success(&response, response_handle);
+    } else {
+        let _ = env.throw_new(
+            "com/android/server/remoteauth/jni/BadHandleException",
+            format!("Failed to find Platform with ID {} in {}", platform_handle, function_name!()),
+        );
+    }
+}
+
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_on_send_request_error(
+    env: JNIEnv,
+    _: JObject,
+    error_code: jint,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    debug!("{}: enter", function_name!());
+    native_on_send_request_error(env, error_code, platform_handle, response_handle);
+}
+
+fn native_on_send_request_error(
+    env: JNIEnv<'_>,
+    error_code: jint,
+    platform_handle: jlong,
+    response_handle: jlong,
+) {
+    if let Some(platform) = HANDLE_MAPPING.lock().unwrap().get(&platform_handle) {
+        let platform = (*platform).lock().unwrap();
+        platform.on_send_request_error(error_code, response_handle);
+    } else {
+        let _ = env.throw_new(
+            "com/android/server/remoteauth/jni/BadHandleException",
+            format!("Failed to find Platform with ID {} in {}", platform_handle, function_name!()),
+        );
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    //use super::*;
+
+    //use tokio::runtime::Builder;
+
+    /// Checks validity of the function_name! macro.
+    #[test]
+    fn test_function_name() {
+        assert_eq!(function_name!(), "test_function_name");
+    }
+}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
new file mode 100644
index 0000000..1f73207
--- /dev/null
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+use crate::unique_jvm;
+use crate::utils::get_boolean_result;
+use jni::objects::JObject;
+use jni::sys::jboolean;
+use jni::JNIEnv;
+
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_remoteauth_jni_NativeRemoteAuthJavaPlatform_native_init(
+    env: JNIEnv,
+    _: JObject,
+) -> jboolean {
+    logger::init(
+        logger::Config::default()
+            .with_tag_on_device("remoteauth")
+            .with_min_level(log::Level::Trace)
+            .with_filter("trace,jni=info"),
+    );
+    get_boolean_result(native_init(env), "native_init")
+}
+
+fn native_init(env: JNIEnv) -> anyhow::Result<()> {
+    let jvm = env.get_java_vm()?;
+    unique_jvm::set_once(jvm)
+}
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
new file mode 100644
index 0000000..46cc361
--- /dev/null
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+//! takes a JavaVM to a static reference.
+//!
+//! JavaVM is shared as multiple JavaVM within a single process is not allowed
+//! per [JNI spec](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html)
+//! The unique JavaVM need to be shared over (potentially) different threads.
+
+use std::sync::{Arc, Once};
+
+use anyhow::Result;
+use jni::JavaVM;
+
+static mut JVM: Option<Arc<JavaVM>> = None;
+static INIT: Once = Once::new();
+/// set_once sets the unique JavaVM that can be then accessed using get_static_ref()
+///
+/// The function shall only be called once.
+pub(crate) fn set_once(jvm: JavaVM) -> Result<()> {
+    // Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
+    // Modification to static mut is nested inside call_once.
+    unsafe {
+        INIT.call_once(|| {
+            JVM = Some(Arc::new(jvm));
+        });
+    }
+    Ok(())
+}
+/// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
+    // Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
+    // Modification to static mut is nested inside call_once.
+    unsafe { JVM.as_ref() }
+}
diff --git a/remoteauth/service/jni/src/utils.rs b/remoteauth/service/jni/src/utils.rs
new file mode 100644
index 0000000..e61b895
--- /dev/null
+++ b/remoteauth/service/jni/src/utils.rs
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+use jni::sys::jboolean;
+use log::error;
+
+pub(crate) fn get_boolean_result<T>(result: anyhow::Result<T>, error_msg: &str) -> jboolean {
+    match result {
+        Ok(_) => true,
+        Err(e) => {
+            error!("{} failed with {:?}", error_msg, &e);
+            false
+        }
+    }
+    .into()
+}
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 4b92d84..37c78c7 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -18,7 +18,10 @@
 
 android_test {
     name: "RemoteAuthUnitTests",
-    defaults: ["mts-target-sdk-version-current"],
+    defaults: [
+        "enable-remoteauth-targets",
+        "mts-target-sdk-version-current"
+    ],
     sdk_version: "test_current",
     min_sdk_version: "31",
 
@@ -35,13 +38,20 @@
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.rules",
+        "com.uwb.support.generic",
         "framework-remoteauth-static",
         "junit",
         "libprotobuf-java-lite",
+        "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "service-remoteauth-pre-jarjar",
         "truth-prebuilt",
     ],
+    // these are needed for Extended Mockito
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
     test_suites: [
         "general-tests",
         "mts-tethering",
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
new file mode 100644
index 0000000..8135b4f
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RangingCapabilities}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingCapabilitiesTest {
+    private static final androidx.core.uwb.backend.impl.internal.RangingCapabilities
+            TEST_UWB_RANGING_CAPABILITIES =
+                    new androidx.core.uwb.backend.impl.internal.RangingCapabilities(
+                            /* supportsDistance= */ true,
+                            /* supportsAzimuthalAngle= */ true,
+                            /* supportsElevationAngle= */ true);
+
+    @Test
+    public void testBuildingRangingCapabilities_success() {
+        final RangingCapabilities rangingCapabilities =
+                new RangingCapabilities.Builder()
+                        .addSupportedRangingMethods(RANGING_METHOD_UWB)
+                        .setUwbRangingCapabilities(TEST_UWB_RANGING_CAPABILITIES)
+                        .build();
+
+        assertEquals(rangingCapabilities.getSupportedRangingMethods().size(), 1);
+        assertEquals(
+                (int) rangingCapabilities.getSupportedRangingMethods().get(0), RANGING_METHOD_UWB);
+        assertEquals(
+                rangingCapabilities.getUwbRangingCapabilities(), TEST_UWB_RANGING_CAPABILITIES);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java
new file mode 100644
index 0000000..6e343bb
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static android.content.pm.PackageManager.FEATURE_UWB;
+import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE;
+
+import static androidx.core.uwb.backend.impl.internal.RangingCapabilities.FIRA_DEFAULT_SUPPORTED_CONFIG_IDS;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_MULTICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR_NO_AOA;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+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.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.uwb.UwbManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/** Unit test for {@link RangingManager}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingManagerTest {
+    private static final List<Integer> TEST_UWB_SUPPORTED_CHANNELS = List.of(8, 9);
+    private static final FiraSpecificationParams TEST_FIRA_SPEC =
+            new FiraSpecificationParams.Builder()
+                    .setSupportedChannels(TEST_UWB_SUPPORTED_CHANNELS)
+                    .setStsCapabilities(EnumSet.allOf(FiraParams.StsCapabilityFlag.class))
+                    .build();
+    private static final GenericSpecificationParams TEST_GENERIC_SPEC =
+            new GenericSpecificationParams.Builder()
+                    .setFiraSpecificationParams(TEST_FIRA_SPEC)
+                    .build();
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final SessionParameters TEST_SESSION_PARAMETER =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+
+    @Mock private Context mContext;
+    @Mock private PackageManager mPackageManager;
+    @Mock private UwbManager mUwbManager;
+
+    private RangingManager mRangingManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getSystemService(UwbManager.class)).thenReturn(mUwbManager);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(false);
+        when(mUwbManager.getAdapterState()).thenReturn(STATE_ENABLED_INACTIVE);
+        when(mUwbManager.getSpecificationInfo()).thenReturn(TEST_GENERIC_SPEC.toBundle());
+    }
+
+    @Test
+    public void testConstruction() {
+        mRangingManager = new RangingManager(mContext);
+        verifyZeroInteractions(mUwbManager);
+    }
+
+    @Test
+    public void testConstruction_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+
+        verify(mUwbManager).getAdapterState();
+        verify(mUwbManager).registerAdapterStateCallback(any(), any());
+    }
+
+    @Test
+    public void testShutdown_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        mRangingManager.shutdown();
+
+        verify(mUwbManager).registerAdapterStateCallback(any(), any());
+        verify(mUwbManager).unregisterAdapterStateCallback(any());
+    }
+
+    @Test
+    public void testGetRangingCapabilities() {
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities = mRangingManager.getRangingCapabilities();
+
+        assertEquals(capabilities.getSupportedRangingMethods().size(), 0);
+        assertEquals(capabilities.getUwbRangingCapabilities(), null);
+    }
+
+    @Test
+    public void testGetRangingCapabilities_withUwbEnabled() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities = mRangingManager.getRangingCapabilities();
+
+        List<Integer> supportedConfigIds = new ArrayList<>(FIRA_DEFAULT_SUPPORTED_CONFIG_IDS);
+        supportedConfigIds.add(CONFIG_PROVISIONED_UNICAST_DS_TWR);
+        supportedConfigIds.add(CONFIG_PROVISIONED_MULTICAST_DS_TWR);
+        supportedConfigIds.add(CONFIG_PROVISIONED_UNICAST_DS_TWR_NO_AOA);
+        supportedConfigIds.add(CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR);
+
+        verify(mUwbManager, times(1)).getSpecificationInfo();
+        assertEquals(capabilities.getSupportedRangingMethods().size(), 1);
+        assertEquals((int) capabilities.getSupportedRangingMethods().get(0), RANGING_METHOD_UWB);
+        androidx.core.uwb.backend.impl.internal.RangingCapabilities uwbCapabilities =
+                capabilities.getUwbRangingCapabilities();
+        assertNotNull(uwbCapabilities);
+        assertArrayEquals(
+                uwbCapabilities.getSupportedChannels().toArray(),
+                TEST_UWB_SUPPORTED_CHANNELS.toArray());
+        assertArrayEquals(
+                uwbCapabilities.getSupportedConfigIds().toArray(), supportedConfigIds.toArray());
+    }
+
+    @Test
+    public void testGetRangingCapabilities_multipleCalls() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+
+        mRangingManager = new RangingManager(mContext);
+        RangingCapabilities capabilities1 = mRangingManager.getRangingCapabilities();
+        RangingCapabilities capabilities2 = mRangingManager.getRangingCapabilities();
+        RangingCapabilities capabilities3 = mRangingManager.getRangingCapabilities();
+
+        verify(mUwbManager, times(1)).getSpecificationInfo();
+        assertEquals(capabilities1, capabilities2);
+        assertEquals(capabilities2, capabilities3);
+    }
+
+    @Test
+    public void testCreateSession_nullSessionParameters() {
+        mRangingManager = new RangingManager(mContext);
+
+        assertThrows(NullPointerException.class, () -> mRangingManager.createSession(null));
+    }
+
+    @Test
+    public void testCreateSession_uwbSessionWithUwbDisabled() {
+        mRangingManager = new RangingManager(mContext);
+
+        assertNull(mRangingManager.createSession(TEST_SESSION_PARAMETER));
+    }
+
+    @Test
+    public void testCreateSession_uwbSession() {
+        when(mPackageManager.hasSystemFeature(FEATURE_UWB)).thenReturn(true);
+        mRangingManager = new RangingManager(mContext);
+
+        assertNotNull(mRangingManager.createSession(TEST_SESSION_PARAMETER));
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java
new file mode 100644
index 0000000..6ac56ea
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingReportTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingReport.ProximityState;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RangingReport}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingReportTest {
+
+    private static final float TEST_DISTANCE_M = 1.5f;
+    @ProximityState private static final int TEST_PROXIMITY_STATE = PROXIMITY_STATE_INSIDE;
+
+    @Test
+    public void testBuildingRangingReport_success() {
+        final RangingReport rangingReport =
+                new RangingReport.Builder()
+                        .setDistanceM(TEST_DISTANCE_M)
+                        .setProximityState(TEST_PROXIMITY_STATE)
+                        .build();
+
+        assertEquals(rangingReport.getDistanceM(), TEST_DISTANCE_M, 0.0f);
+        assertEquals(rangingReport.getProximityState(), TEST_PROXIMITY_STATE);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java
new file mode 100644
index 0000000..0e547d6
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingSessionTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link RangingSession}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingSessionTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_BASE_KEY2 =
+            new byte[] {
+                0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0,
+                0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final byte[] TEST_SYNC_DATA2 =
+            new byte[] {
+                0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+
+    private static final SessionParameters TEST_SESSION_PARAMETER_WITH_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(true)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_WO_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(TEST_DEVICE_ROLE)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(false)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final int TEST_DERIVE_DATA_LENGTH = 40;
+
+    /** Wrapper class for testing {@link RangingSession}. */
+    public static class RangingSessionWrapper extends RangingSession {
+        public RangingSessionWrapper(
+                Context context, SessionParameters sessionParameters, int derivedDataLength) {
+            super(context, sessionParameters, derivedDataLength);
+        }
+
+        @Override
+        public void start(
+                RangingParameters rangingParameters,
+                Executor executor,
+                RangingCallback rangingCallback) {}
+
+        @Override
+        public void stop() {}
+
+        @Override
+        public boolean updateDerivedData() {
+            return super.updateDerivedData();
+        }
+
+        public byte[] baseKey() {
+            return mBaseKey;
+        }
+
+        public byte[] syncData() {
+            return mSyncData;
+        }
+
+        public byte[] derivedData() {
+            return mDerivedData;
+        }
+
+        public int syncCounter() {
+            return mSyncCounter;
+        }
+    }
+
+    @Mock private Context mContext;
+
+    private RangingSessionWrapper mRangingSessionWithAD;
+    private RangingSessionWrapper mRangingSessionWithoutAD;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mRangingSessionWithAD =
+                new RangingSessionWrapper(
+                        mContext, TEST_SESSION_PARAMETER_WITH_AD, TEST_DERIVE_DATA_LENGTH);
+        mRangingSessionWithoutAD =
+                new RangingSessionWrapper(mContext, TEST_SESSION_PARAMETER_WO_AD, 0);
+    }
+
+    @Test
+    public void testResetBaseKey_autoDeriveDisabled() {
+        assertNull(mRangingSessionWithoutAD.baseKey());
+        mRangingSessionWithoutAD.resetBaseKey(TEST_BASE_KEY2);
+        assertNull(mRangingSessionWithoutAD.baseKey());
+    }
+
+    @Test
+    public void testResetBaseKey_nullBaseKey() {
+        assertThrows(NullPointerException.class, () -> mRangingSessionWithAD.resetBaseKey(null));
+    }
+
+    @Test
+    public void testResetBaseKey_invalidBaseKey() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mRangingSessionWithAD.resetBaseKey(new byte[] {0x1, 0x2, 0x3, 0x4}));
+    }
+
+    @Test
+    public void testResetBaseKey_success() {
+        mRangingSessionWithAD.resetBaseKey(TEST_BASE_KEY2);
+        assertArrayEquals(mRangingSessionWithAD.baseKey(), TEST_BASE_KEY2);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 2);
+
+        mRangingSessionWithAD.resetBaseKey(TEST_BASE_KEY);
+        assertArrayEquals(mRangingSessionWithAD.baseKey(), TEST_BASE_KEY);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 3);
+    }
+
+    @Test
+    public void testResetSyncData_autoDeriveDisabled() {
+        assertNull(mRangingSessionWithoutAD.syncData());
+        mRangingSessionWithoutAD.resetSyncData(TEST_SYNC_DATA2);
+        assertNull(mRangingSessionWithoutAD.syncData());
+    }
+
+    @Test
+    public void testResetSyncData_nullSyncData() {
+        assertThrows(NullPointerException.class, () -> mRangingSessionWithAD.resetSyncData(null));
+    }
+
+    @Test
+    public void testResetSyncData_invalidSyncData() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mRangingSessionWithAD.resetSyncData(new byte[] {0x1, 0x2, 0x3, 0x4}));
+    }
+
+    @Test
+    public void testResetSyncData_success() {
+        mRangingSessionWithAD.resetSyncData(TEST_SYNC_DATA2);
+        assertArrayEquals(mRangingSessionWithAD.syncData(), TEST_SYNC_DATA2);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 1);
+
+        mRangingSessionWithAD.resetSyncData(TEST_SYNC_DATA);
+        assertArrayEquals(mRangingSessionWithAD.syncData(), TEST_SYNC_DATA);
+        assertEquals(mRangingSessionWithAD.syncCounter(), 1);
+    }
+
+    @Test
+    public void testUpdateDerivedData_autoDeriveDisabled() {
+        assertFalse(mRangingSessionWithoutAD.updateDerivedData());
+        assertEquals(mRangingSessionWithoutAD.syncCounter(), 0);
+    }
+
+    @Test
+    public void testUpdateDerivedData_hkdfFailed() {
+        // Max derivedDataLength is 32*255
+        RangingSessionWrapper rangingSession =
+                new RangingSessionWrapper(
+                        mContext, TEST_SESSION_PARAMETER_WITH_AD, /* derivedDataLength= */ 10000);
+        assertNull(rangingSession.derivedData());
+        assertFalse(rangingSession.updateDerivedData());
+        assertEquals(rangingSession.syncCounter(), 0);
+        assertNull(rangingSession.derivedData());
+    }
+
+    @Test
+    public void testUpdateDerivedData_success() {
+        assertNotNull(mRangingSessionWithAD.derivedData());
+        assertTrue(mRangingSessionWithAD.updateDerivedData());
+        assertEquals(mRangingSessionWithAD.syncCounter(), 2);
+        assertNotNull(mRangingSessionWithAD.derivedData());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java
new file mode 100644
index 0000000..9364092
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionInfoTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link SessionInfo}. */
+@RunWith(AndroidJUnit4.class)
+public class SessionInfoTest {
+
+    private static final String TEST_DEVICE_ID = new String("test_device_id");
+    private static final @RangingMethod int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+
+    @Test
+    public void testBuildingSessionInfo_success() {
+        final SessionInfo sessionInfo =
+                new SessionInfo.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .build();
+
+        assertEquals(sessionInfo.getDeviceId(), TEST_DEVICE_ID);
+        assertEquals(sessionInfo.getRangingMethod(), TEST_RANGING_METHOD);
+    }
+
+    @Test
+    public void testBuildingSessionInfo_invalidDeviceId() {
+        final SessionInfo.Builder builder =
+                new SessionInfo.Builder().setRangingMethod(TEST_RANGING_METHOD);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionInfo_invalidRangingMethod() {
+        final SessionInfo.Builder builder = new SessionInfo.Builder().setDeviceId(TEST_DEVICE_ID);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
new file mode 100644
index 0000000..522623e
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.ranging;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.SessionParameters.DeviceRole;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link SessionParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class SessionParametersTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    @DeviceRole private static final int TEST_DEVICE_ROLE = DEVICE_ROLE_INITIATOR;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+
+    @Test
+    public void testBuildingSessionParameters_success() {
+        final SessionParameters sessionParameters =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA)
+                        .build();
+
+        assertEquals(sessionParameters.getDeviceId(), TEST_DEVICE_ID);
+        assertEquals(sessionParameters.getRangingMethod(), TEST_RANGING_METHOD);
+        assertEquals(
+                sessionParameters.getLowerProximityBoundaryM(),
+                TEST_LOWER_PROXIMITY_BOUNDARY_M,
+                0.0f);
+        assertEquals(
+                sessionParameters.getUpperProximityBoundaryM(),
+                TEST_UPPER_PROXIMITY_BOUNDARY_M,
+                0.0f);
+        assertEquals(sessionParameters.getAutoDeriveParams(), TEST_AUTO_DERIVE_PARAMS);
+        assertArrayEquals(sessionParameters.getBaseKey(), TEST_BASE_KEY);
+        assertArrayEquals(sessionParameters.getSyncData(), TEST_SYNC_DATA);
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidDeviceId() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidRangingMethod() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidDeviceRole() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidLowerProximityBoundaryM() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(-1.0f)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidUpperProximityBoundaryM() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M - 0.1f)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_disableAutoDeriveParams() {
+        final boolean autoDeriveParams = false;
+        final SessionParameters sessionParameters =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(autoDeriveParams)
+                        .build();
+
+        assertEquals(sessionParameters.getAutoDeriveParams(), autoDeriveParams);
+        assertArrayEquals(sessionParameters.getBaseKey(), new byte[] {});
+        assertArrayEquals(sessionParameters.getSyncData(), new byte[] {});
+    }
+
+    @Test
+    public void testBuildingSessionParameters_emptyBaseKey() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidBaseKey() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(new byte[] {0x00, 0x01, 0x02, 0x13})
+                        .setSyncData(TEST_SYNC_DATA);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_emptySyncData() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY);
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testBuildingSessionParameters_invalidSyncData() {
+        final SessionParameters.Builder builder =
+                new SessionParameters.Builder()
+                        .setDeviceId(TEST_DEVICE_ID)
+                        .setRangingMethod(TEST_RANGING_METHOD)
+                        .setDeviceRole(TEST_DEVICE_ROLE)
+                        .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                        .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                        .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                        .setBaseKey(TEST_BASE_KEY)
+                        .setSyncData(new byte[] {0x00, 0x01, 0x02, 0x13});
+
+        assertThrows(IllegalArgumentException.class, () -> builder.build());
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java
new file mode 100644
index 0000000..eb7a8c5
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/util/CryptoTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link Crypto}. */
+@RunWith(AndroidJUnit4.class)
+public class CryptoTest {
+    private static final byte[] TEST_IKM =
+            new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+    private static final byte[] TEST_SALT =
+            new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00};
+    private static final int TEST_SIZE = 40;
+
+    @Test
+    public void testComputeHkdf_exceedMaxSize() {
+        // Max size is 32*255
+        assertNull(Crypto.computeHkdf(TEST_IKM, TEST_SALT, /* size= */ 10000));
+    }
+
+    @Test
+    public void testComputeHkdf_emptySalt() {
+        assertNull(Crypto.computeHkdf(TEST_IKM, new byte[] {}, TEST_SIZE));
+    }
+
+    @Test
+    public void testComputeHkdf_emptyIkm() {
+        assertNull(Crypto.computeHkdf(new byte[] {}, TEST_SALT, TEST_SIZE));
+    }
+
+    @Test
+    public void testComputeHkdf_success() {
+        assertNotNull(Crypto.computeHkdf(TEST_IKM, TEST_SALT, TEST_SIZE));
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 83caf35..08527a3 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -19,6 +19,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+service_remoteauth_pre_jarjar_lib = "service-remoteauth-pre-jarjar"
+
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
@@ -56,7 +58,7 @@
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
         "service-thread-pre-jarjar",
-        "service-remoteauth-pre-jarjar",
+        service_remoteauth_pre_jarjar_lib,
         "ServiceConnectivityResources",
         "unsupportedappusage",
     ],
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 597c06f..6b03daa 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -24,16 +24,22 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.ConnectivityStatsLog;
 
+import java.util.Random;
+
 /**
  * Class to record the NetworkNsdReported into statsd. Each client should create this class to
  * report its data.
  */
 public class NetworkNsdReportedMetrics {
+    // The upper bound for the random number used in metrics data sampling determines the possible
+    // sample rate.
+    private static final int RANDOM_NUMBER_UPPER_BOUND = 1000;
     // Whether this client is using legacy backend.
     private final boolean mIsLegacy;
     // The client id.
     private final int mClientId;
     private final Dependencies mDependencies;
+    private final Random mRandom;
 
     public NetworkNsdReportedMetrics(boolean isLegacy, int clientId) {
         this(isLegacy, clientId, new Dependencies());
@@ -44,6 +50,7 @@
         mIsLegacy = isLegacy;
         mClientId = clientId;
         mDependencies = dependencies;
+        mRandom = dependencies.makeRandomGenerator();
     }
 
     /**
@@ -67,7 +74,18 @@
                     event.getFoundCallbackCount(),
                     event.getLostCallbackCount(),
                     event.getRepliedRequestsCount(),
-                    event.getSentQueryCount());
+                    event.getSentQueryCount(),
+                    event.getSentPacketCount(),
+                    event.getConflictDuringProbingCount(),
+                    event.getConflictAfterProbingCount(),
+                    event.getRandomNumber());
+        }
+
+        /**
+         * @see Random
+         */
+        public Random makeRandomGenerator() {
+            return new Random();
         }
     }
 
@@ -75,6 +93,7 @@
         final Builder builder = NetworkNsdReported.newBuilder();
         builder.setIsLegacy(mIsLegacy);
         builder.setClientId(mClientId);
+        builder.setRandomNumber(mRandom.nextInt(RANDOM_NUMBER_UPPER_BOUND));
         return builder;
     }
 
@@ -113,14 +132,23 @@
      *
      * @param transactionId The transaction id of service registration.
      * @param durationMs The duration of service stayed registered.
+     * @param repliedRequestsCount The replied request count of this service before unregistered it.
+     * @param sentPacketCount The total sent packet count of this service before unregistered it.
+     * @param conflictDuringProbingCount The number of conflict during probing.
+     * @param conflictAfterProbingCount The number of conflict after probing.
      */
-    public void reportServiceUnregistration(int transactionId, long durationMs) {
+    public void reportServiceUnregistration(int transactionId, long durationMs,
+            int repliedRequestsCount, int sentPacketCount, int conflictDuringProbingCount,
+            int conflictAfterProbingCount) {
         final Builder builder = makeReportedBuilder();
         builder.setTransactionId(transactionId);
         builder.setType(NsdEventType.NET_REGISTER);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_UNREGISTERED);
         builder.setEventDurationMillisec(durationMs);
-        // TODO: Report repliedRequestsCount
+        builder.setRepliedRequestsCount(repliedRequestsCount);
+        builder.setSentPacketCount(sentPacketCount);
+        builder.setConflictDuringProbingCount(conflictDuringProbingCount);
+        builder.setConflictAfterProbingCount(conflictAfterProbingCount);
         mDependencies.statsWrite(builder.build());
     }
 
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 2da067a..624c5df 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -17,7 +17,6 @@
 package com.android.server;
 
 import android.content.Context;
-import android.remoteauth.RemoteAuthManager;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -90,8 +89,8 @@
         }
 
         if (mRemoteAuthService != null) {
-            Log.i(TAG, "Registering " + RemoteAuthManager.REMOTE_AUTH_SERVICE);
-            publishBinderService(RemoteAuthManager.REMOTE_AUTH_SERVICE, mRemoteAuthService,
+            Log.i(TAG, "Registering " + RemoteAuthService.SERVICE_NAME);
+            publishBinderService(RemoteAuthService.SERVICE_NAME, mRemoteAuthService,
                     /* allowIsolated= */ false);
         }
     }
@@ -157,8 +156,7 @@
         } catch (UnsupportedOperationException e) {
             // RemoteAuth is not yet supported in all branches
             // TODO: remove catch clause when it is available.
-            Log.i(TAG, "Skipping unsupported service "
-                    + RemoteAuthManager.REMOTE_AUTH_SERVICE);
+            Log.i(TAG, "Skipping unsupported service " + RemoteAuthService.SERVICE_NAME);
             return null;
         }
     }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 6485e99..b9acc48 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -17,15 +17,20 @@
 package com.android.server;
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
 import static android.net.ConnectivityManager.NETID_UNSET;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
 import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
 import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
+import static com.android.networkstack.apishim.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
+import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 
@@ -75,6 +80,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.metrics.NetworkNsdReportedMetrics;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InetAddressUtils;
@@ -988,14 +994,20 @@
                         // instead of looking at the flag value.
                         final long stopTimeMs = mClock.elapsedRealtime();
                         if (request instanceof AdvertiserClientRequest) {
+                            final AdvertiserMetrics metrics =
+                                    mAdvertiser.getAdvertiserMetrics(transactionId);
                             mAdvertiser.removeService(transactionId);
                             clientInfo.onUnregisterServiceSucceeded(clientRequestId, transactionId,
-                                    request.calculateRequestDurationMs(stopTimeMs));
+                                    request.calculateRequestDurationMs(stopTimeMs), metrics);
                         } else {
                             if (unregisterService(transactionId)) {
                                 clientInfo.onUnregisterServiceSucceeded(clientRequestId,
                                         transactionId,
-                                        request.calculateRequestDurationMs(stopTimeMs));
+                                        request.calculateRequestDurationMs(stopTimeMs),
+                                        new AdvertiserMetrics(NO_PACKET /* repliedRequestsCount */,
+                                                NO_PACKET /* sentPacketCount */,
+                                                0 /* conflictDuringProbingCount */,
+                                                0 /* conflictAfterProbingCount */));
                             } else {
                                 clientInfo.onUnregisterServiceFailed(
                                         clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
@@ -1191,7 +1203,7 @@
                         // TODO: Limits the number of registrations created by a given class.
                         mOffloadEngines.register(offloadEngineInfo.mOffloadEngine,
                                 offloadEngineInfo);
-                        // TODO: Sends all the existing OffloadServiceInfos back.
+                        sendAllOffloadServiceInfos(offloadEngineInfo);
                         break;
                     case NsdManager.UNREGISTER_OFFLOAD_ENGINE:
                         mOffloadEngines.unregister((IOffloadEngine) msg.obj);
@@ -1877,6 +1889,21 @@
         }
     }
 
+    private void sendAllOffloadServiceInfos(@NonNull OffloadEngineInfo offloadEngineInfo) {
+        final String targetInterface = offloadEngineInfo.mInterfaceName;
+        final IOffloadEngine offloadEngine = offloadEngineInfo.mOffloadEngine;
+        final List<MdnsAdvertiser.OffloadServiceInfoWrapper> offloadWrappers =
+                mAdvertiser.getAllInterfaceOffloadServiceInfos(targetInterface);
+        for (MdnsAdvertiser.OffloadServiceInfoWrapper wrapper : offloadWrappers) {
+            try {
+                offloadEngine.onOffloadServiceUpdated(wrapper.mOffloadServiceInfo);
+            } catch (RemoteException e) {
+                // Can happen in regular cases, do not log a stacktrace
+                Log.i(TAG, "Failed to send offload callback, remote died: " + e.getMessage());
+            }
+        }
+    }
+
     private void sendOffloadServiceInfosUpdate(@NonNull String targetInterfaceName,
             @NonNull OffloadServiceInfo offloadServiceInfo, boolean isRemove) {
         final int count = mOffloadEngines.beginBroadcast();
@@ -1900,7 +1927,7 @@
                     }
                 } catch (RemoteException e) {
                     // Can happen in regular cases, do not log a stacktrace
-                    Log.i(TAG, "Failed to send offload callback, remote died", e);
+                    Log.i(TAG, "Failed to send offload callback, remote died: " + e.getMessage());
                 }
             }
         } finally {
@@ -2083,9 +2110,7 @@
         public void registerOffloadEngine(String ifaceName, IOffloadEngine cb,
                 @OffloadEngine.OffloadCapability long offloadCapabilities,
                 @OffloadEngine.OffloadType long offloadTypes) {
-            // TODO: Relax the permission because NETWORK_SETTINGS is a signature permission, and
-            //  it may not be possible for all the callers of this API to have it.
-            PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
+            checkOffloadEnginePermission(mContext);
             Objects.requireNonNull(ifaceName);
             Objects.requireNonNull(cb);
             mNsdStateMachine.sendMessage(
@@ -2096,13 +2121,31 @@
 
         @Override
         public void unregisterOffloadEngine(IOffloadEngine cb) {
-            // TODO: Relax the permission because NETWORK_SETTINGS is a signature permission, and
-            //  it may not be possible for all the callers of this API to have it.
-            PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
+            checkOffloadEnginePermission(mContext);
             Objects.requireNonNull(cb);
             mNsdStateMachine.sendMessage(
                     mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_OFFLOAD_ENGINE, cb));
         }
+
+        private static void checkOffloadEnginePermission(Context context) {
+            if (!SdkLevel.isAtLeastT()) {
+                throw new SecurityException("API is not available in before API level 33");
+            }
+            // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V, but may
+            // be back ported to older builds: accept it as long as it's signature-protected
+            if (PermissionUtils.checkAnyPermissionOf(context, REGISTER_NSD_OFFLOAD_ENGINE)
+                    && (SdkLevel.isAtLeastV() || PermissionUtils.isSystemSignaturePermission(
+                    context, REGISTER_NSD_OFFLOAD_ENGINE))) {
+                return;
+            }
+            if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
+                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+                return;
+            }
+            throw new SecurityException("Requires one of the following permissions: "
+                    + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
+                    PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+        }
     }
 
     private void sendNsdStateChangeBroadcast(boolean isEnabled) {
@@ -2461,9 +2504,14 @@
                 }
 
                 if (request instanceof AdvertiserClientRequest) {
+                    final AdvertiserMetrics metrics =
+                            mAdvertiser.getAdvertiserMetrics(transactionId);
                     mAdvertiser.removeService(transactionId);
                     mMetrics.reportServiceUnregistration(transactionId,
-                            request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                            request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                            metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                            metrics.mConflictDuringProbingCount,
+                            metrics.mConflictAfterProbingCount);
                     continue;
                 }
 
@@ -2489,7 +2537,11 @@
                     case NsdManager.REGISTER_SERVICE:
                         unregisterService(transactionId);
                         mMetrics.reportServiceUnregistration(transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_PACKET /* repliedRequestsCount */,
+                                NO_PACKET /* sentPacketCount */,
+                                0 /* conflictDuringProbingCount */,
+                                0 /* conflictAfterProbingCount */);
                         break;
                     default:
                         break;
@@ -2628,8 +2680,11 @@
             }
         }
 
-        void onUnregisterServiceSucceeded(int listenerKey, int transactionId, long durationMs) {
-            mMetrics.reportServiceUnregistration(transactionId, durationMs);
+        void onUnregisterServiceSucceeded(int listenerKey, int transactionId, long durationMs,
+                AdvertiserMetrics metrics) {
+            mMetrics.reportServiceUnregistration(transactionId, durationMs,
+                    metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                    metrics.mConflictDuringProbingCount, metrics.mConflictAfterProbingCount);
             try {
                 mCb.onUnregisterServiceSucceeded(listenerKey);
             } catch (RemoteException e) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index dd72d11..913d233 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
 
 import android.annotation.NonNull;
@@ -37,6 +38,7 @@
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -117,6 +119,17 @@
         }
     }
 
+    /**
+     * Gets the current status of the OffloadServiceInfos per interface.
+     * @param interfaceName the target interfaceName
+     * @return the list of current offloaded services.
+     */
+    @NonNull
+    public List<OffloadServiceInfoWrapper> getAllInterfaceOffloadServiceInfos(
+            @NonNull String interfaceName) {
+        return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
+    }
+
     private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
             new MdnsInterfaceAdvertiser.Callback() {
         @Override
@@ -134,9 +147,12 @@
                             interfaceName, k -> new ArrayList<>());
             // Remove existing offload services from cache for update.
             existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
+
+            byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
             final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
                     serviceId,
-                    registration);
+                    registration,
+                    rawOffloadPacket);
             existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
             mCb.onOffloadStartOrUpdate(interfaceName,
                     newOffloadServiceInfoWrapper.mOffloadServiceInfo);
@@ -165,6 +181,7 @@
                 // (with the old, conflicting, actually not used name as argument... The new
                 // implementation will send callbacks with the new name).
                 registration.mNotifiedRegistrationSuccess = false;
+                registration.mConflictAfterProbingCount++;
 
                 // The service was done probing, just reset it to probing state (RFC6762 9.)
                 forAllAdvertisers(a -> {
@@ -180,6 +197,7 @@
             registration.updateForConflict(
                     registration.makeNewServiceInfoForConflict(1 /* renameCount */),
                     1 /* renameCount */);
+            registration.mConflictDuringProbingCount++;
 
             // Keep renaming if the new name conflicts in local registrations
             updateRegistrationUntilNoConflict((net, adv) -> adv.hasRegistration(registration),
@@ -345,6 +363,22 @@
             }
         }
 
+        int getServiceRepliedRequestsCount(int id) {
+            int repliedRequestsCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                repliedRequestsCount += mAdvertisers.valueAt(i).getServiceRepliedRequestsCount(id);
+            }
+            return repliedRequestsCount;
+        }
+
+        int getSentPacketCount(int id) {
+            int sentPacketCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                sentPacketCount += mAdvertisers.valueAt(i).getSentPacketCount(id);
+            }
+            return sentPacketCount;
+        }
+
         @Override
         public void onSocketCreated(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket,
@@ -381,13 +415,38 @@
         public void onAddressesChanged(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
-            if (advertiser != null) advertiser.updateAddresses(addresses);
+            if (advertiser == null)  {
+                return;
+            }
+            advertiser.updateAddresses(addresses);
+            // Update address should trigger offload packet update.
+            final String interfaceName = advertiser.getSocketInterfaceName();
+            final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                    mInterfaceOffloadServices.get(interfaceName);
+            if (existingOffloadServiceInfoWrappers == null) {
+                return;
+            }
+            final List<OffloadServiceInfoWrapper> updatedOffloadServiceInfoWrappers =
+                    new ArrayList<>(existingOffloadServiceInfoWrappers.size());
+            for (OffloadServiceInfoWrapper oldWrapper : existingOffloadServiceInfoWrappers) {
+                OffloadServiceInfoWrapper newWrapper = new OffloadServiceInfoWrapper(
+                        oldWrapper.mServiceId,
+                        oldWrapper.mOffloadServiceInfo.withOffloadPayload(
+                                advertiser.getRawOffloadPayload(oldWrapper.mServiceId))
+                );
+                updatedOffloadServiceInfoWrappers.add(newWrapper);
+                mCb.onOffloadStartOrUpdate(interfaceName, newWrapper.mOffloadServiceInfo);
+            }
+            mInterfaceOffloadServices.put(interfaceName, updatedOffloadServiceInfoWrappers);
         }
     }
 
-    private static class OffloadServiceInfoWrapper {
-        private final @NonNull OffloadServiceInfo mOffloadServiceInfo;
-        private final int mServiceId;
+    /**
+     * The wrapper class for OffloadServiceInfo including the serviceId.
+     */
+    public static class OffloadServiceInfoWrapper {
+        public final @NonNull OffloadServiceInfo mOffloadServiceInfo;
+        public final int mServiceId;
 
         OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo) {
             mOffloadServiceInfo = offloadServiceInfo;
@@ -404,6 +463,8 @@
         private NsdServiceInfo mServiceInfo;
         @Nullable
         private final String mSubtype;
+        int mConflictDuringProbingCount;
+        int mConflictAfterProbingCount;
 
         private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
             this.mOriginalName = serviceInfo.getServiceName();
@@ -515,6 +576,24 @@
                 @NonNull OffloadServiceInfo offloadServiceInfo);
     }
 
+    /**
+     * Data class of avdverting metrics.
+     */
+    public static class AdvertiserMetrics {
+        public final int mRepliedRequestsCount;
+        public final int mSentPacketCount;
+        public final int mConflictDuringProbingCount;
+        public final int mConflictAfterProbingCount;
+
+        public AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount,
+                int conflictDuringProbingCount, int conflictAfterProbingCount) {
+            mRepliedRequestsCount = repliedRequestsCount;
+            mSentPacketCount = sentPacketCount;
+            mConflictDuringProbingCount = conflictDuringProbingCount;
+            mConflictAfterProbingCount = conflictAfterProbingCount;
+        }
+    }
+
     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
             @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
         this(looper, socketProvider, cb, new Dependencies(), sharedLog);
@@ -597,6 +676,34 @@
         }
     }
 
+    /**
+     * Get advertising metrics.
+     *
+     * @param id ID used when registering.
+     * @return The advertising metrics includes replied requests count, send packet count, conflict
+     *         count during/after probing.
+     */
+    public AdvertiserMetrics getAdvertiserMetrics(int id) {
+        checkThread();
+        final Registration registration = mRegistrations.get(id);
+        if (registration == null) {
+            return new AdvertiserMetrics(
+                    NO_PACKET /* repliedRequestsCount */,
+                    NO_PACKET /* sentPacketCount */,
+                    0 /* conflictDuringProbingCount */,
+                    0 /* conflictAfterProbingCount */);
+        }
+        int repliedRequestsCount = NO_PACKET;
+        int sentPacketCount = NO_PACKET;
+        for (int i = 0; i < mAdvertiserRequests.size(); i++) {
+            repliedRequestsCount +=
+                    mAdvertiserRequests.valueAt(i).getServiceRepliedRequestsCount(id);
+            sentPacketCount += mAdvertiserRequests.valueAt(i).getSentPacketCount(id);
+        }
+        return new AdvertiserMetrics(repliedRequestsCount, sentPacketCount,
+                registration.mConflictDuringProbingCount, registration.mConflictAfterProbingCount);
+    }
+
     private static <K, V> boolean any(@NonNull ArrayMap<K, V> map,
             @NonNull BiPredicate<K, V> predicate) {
         for (int i = 0; i < map.size(); i++) {
@@ -615,9 +722,9 @@
     }
 
     private OffloadServiceInfoWrapper createOffloadService(int serviceId,
-            @NonNull Registration registration) {
+            @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
-        List<String> subTypes = new ArrayList<>();
+        final List<String> subTypes = new ArrayList<>();
         String subType = registration.getSubtype();
         if (subType != null) {
             subTypes.add(subType);
@@ -627,7 +734,7 @@
                         nsdServiceInfo.getServiceType()),
                 subTypes,
                 String.join(".", mDeviceHostName),
-                null /* rawOffloadPacket */,
+                rawOffloadPacket,
                 // TODO: define overlayable resources in
                 // ServiceConnectivityResources that set the priority based on
                 // service type.
@@ -636,5 +743,4 @@
                 OffloadEngine.OFFLOAD_TYPE_REPLY);
         return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
     }
-
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 0c32cf1..1251170 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -37,6 +37,7 @@
     public static final int FLAG_TRUNCATED = 0x0200;
     public static final int QCLASS_INTERNET = 0x0001;
     public static final int QCLASS_UNICAST = 0x8000;
+    public static final int NO_PACKET = 0;
     public static final String SUBTYPE_LABEL = "_sub";
     public static final String SUBTYPE_PREFIX = "_";
     private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index a83b852..6454959 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkAddress;
@@ -28,6 +30,7 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo;
 import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -92,8 +95,11 @@
     /**
      * Callbacks from {@link MdnsProber}.
      */
-    private class ProbingCallback implements
-            PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+    private class ProbingCallback implements PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+        @Override
+        public void onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount) {
+            mRecordRepository.onProbingSent(info.getServiceId(), sentPacketCount);
+        }
         @Override
         public void onFinished(MdnsProber.ProbingInfo info) {
             final MdnsAnnouncer.AnnouncementInfo announcementInfo;
@@ -117,8 +123,8 @@
      */
     private class AnnouncingCallback implements PacketRepeaterCallback<BaseAnnouncementInfo> {
         @Override
-        public void onSent(int index, @NonNull BaseAnnouncementInfo info) {
-            mRecordRepository.onAdvertisementSent(info.getServiceId());
+        public void onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount) {
+            mRecordRepository.onAdvertisementSent(info.getServiceId(), sentPacketCount);
         }
 
         @Override
@@ -259,6 +265,22 @@
     }
 
     /**
+     * Get the replied request count from given service id.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getServiceRepliedRequestsCount(id);
+    }
+
+    /**
+     * Get the total sent packet count from given service id.
+     */
+    public int getSentPacketCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getSentPacketCount(id);
+    }
+
+    /**
      * Update interface addresses used to advertise.
      *
      * This causes new address records to be announced.
@@ -351,7 +373,25 @@
         mReplySender.queueReply(answers);
     }
 
+    /**
+     * Get the socket interface name.
+     */
     public String getSocketInterfaceName() {
         return mSocket.getInterface().getName();
     }
+
+    /**
+     * Gets the offload MdnsPacket.
+     * @param serviceId The serviceId.
+     * @return the raw offload payload
+     */
+    public byte[] getRawOffloadPayload(int serviceId) {
+        try {
+            return MdnsUtils.createRawDnsPacket(mReplySender.getPacketCreationBuffer(),
+                    mRecordRepository.getOffloadPacket(serviceId));
+        } catch (IOException | IllegalArgumentException e) {
+            mSharedLog.wtf("Cannot create rawOffloadPacket: " + e.getMessage());
+            return new byte[0];
+        }
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index 644560c..12ed139 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -59,7 +59,7 @@
         /**
          * Called when a packet was sent.
          */
-        default void onSent(int index, @NonNull T info) {}
+        default void onSent(int index, @NonNull T info, int sentPacketCount) {}
 
         /**
          * Called when the {@link MdnsPacketRepeater} is done sending packets.
@@ -114,9 +114,10 @@
             }
             // Send to both v4 and v6 addresses; the reply sender will take care of ignoring the
             // send when the socket has not joined the relevant group.
+            int sentPacketCount = 0;
             for (InetSocketAddress destination : ALL_ADDRS) {
                 try {
-                    mReplySender.sendNow(packet, destination);
+                    sentPacketCount += mReplySender.sendNow(packet, destination);
                 } catch (IOException e) {
                     mSharedLog.e("Error sending packet to " + destination, e);
                 }
@@ -135,7 +136,7 @@
 
             // Call onSent after scheduling the next run, to allow the callback to cancel it
             if (mCb != null) {
-                mCb.onSent(index, request);
+                mCb.onSent(index, request, sentPacketCount);
             }
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 1375279..1fb4d90 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TargetApi;
@@ -175,13 +177,23 @@
         public boolean exiting = false;
 
         /**
+         * The replied query packet count of this service.
+         */
+        public int repliedServiceCount = NO_PACKET;
+
+        /**
+         * The sent packet count of this service (including announcements and probes).
+         */
+        public int sentPacketCount = NO_PACKET;
+
+        /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
          *
          * @param deviceHostname Hostname of the device (for the interface used)
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype) {
+                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
             this.serviceInfo = serviceInfo;
             this.subtype = subtype;
 
@@ -254,6 +266,8 @@
                     true /* sharedName */, true /* probing */));
 
             this.allRecords = Collections.unmodifiableList(allRecords);
+            this.repliedServiceCount = repliedServiceCount;
+            this.sentPacketCount = sentPacketCount;
         }
 
         void setProbing(boolean probing) {
@@ -316,7 +330,8 @@
         }
 
         final ServiceRegistration registration = new ServiceRegistration(
-                mDeviceHostname, serviceInfo, subtype);
+                mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+                NO_PACKET /* sentPacketCount */);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -406,6 +421,24 @@
     }
 
     /**
+     * @return The replied request count of the service.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.repliedServiceCount;
+    }
+
+    /**
+     * @return The total sent packet count of the service.
+     */
+    public int getSentPacketCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.sentPacketCount;
+    }
+
+    /**
      * Remove all services from the repository
      * @return IDs of the removed services
      */
@@ -472,9 +505,12 @@
             for (int i = 0; i < mServices.size(); i++) {
                 final ServiceRegistration registration = mServices.valueAt(i);
                 if (registration.exiting) continue;
-                addReplyFromService(question, registration.allRecords, registration.ptrRecords,
+                if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
                         registration.srvRecord, registration.txtRecord, replyUnicast, now,
-                        answerInfo, additionalAnswerRecords);
+                        answerInfo, additionalAnswerRecords)) {
+                    registration.repliedServiceCount++;
+                    registration.sentPacketCount++;
+                }
             }
         }
 
@@ -527,7 +563,7 @@
     /**
      * Add answers and additional answers for a question, from a ServiceRegistration.
      */
-    private void addReplyFromService(@NonNull MdnsRecord question,
+    private boolean addReplyFromService(@NonNull MdnsRecord question,
             @NonNull List<RecordInfo<?>> serviceRecords,
             @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
             @Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@@ -596,7 +632,7 @@
         }
 
         // No more records to add if no answer
-        if (answerInfo.size() == answersStartIndex) return;
+        if (answerInfo.size() == answersStartIndex) return false;
 
         final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
         // RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
@@ -626,6 +662,7 @@
         addNsecRecordsForUniqueNames(additionalAnswerRecords,
                 answerInfo.listIterator(answersStartIndex),
                 additionalAnswerInfo.listIterator());
+        return true;
     }
 
     /**
@@ -736,6 +773,38 @@
     }
 
     /**
+     * Gets the offload MdnsPacket.
+     * @param serviceId The serviceId.
+     * @return The offload {@link MdnsPacket} that contains PTR/SRV/TXT/A/AAAA records.
+     */
+    public MdnsPacket getOffloadPacket(int serviceId) throws IllegalArgumentException {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) throw new IllegalArgumentException(
+                "Service is not registered: " + serviceId);
+
+        final ArrayList<MdnsRecord> answers = new ArrayList<>();
+
+        // Adds all PTR, SRV, TXT, A/AAAA records.
+        for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
+            answers.add(ptrRecord.record);
+        }
+        answers.add(registration.srvRecord.record);
+        answers.add(registration.txtRecord.record);
+        for (RecordInfo<?> record : mGeneralRecords) {
+            if (record.record instanceof MdnsInetAddressRecord) {
+                answers.add(record.record);
+            }
+        }
+
+        final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
+        return new MdnsPacket(flags,
+                Collections.emptyList() /* questions */,
+                answers,
+                Collections.emptyList() /* authorityRecords */,
+                Collections.emptyList() /* additionalRecords */);
+    }
+
+    /**
      * Get the service IDs of services conflicting with a received packet.
      */
     public Set<Integer> getConflictingServices(MdnsPacket packet) {
@@ -830,8 +899,8 @@
         final ServiceRegistration existing = mServices.get(serviceId);
         if (existing == null) return null;
 
-        final ServiceRegistration newService = new ServiceRegistration(
-                mDeviceHostname, newInfo, existing.subtype);
+        final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
+                existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService.srvRecord.record);
     }
@@ -839,7 +908,7 @@
     /**
      * Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
      */
-    public void onAdvertisementSent(int serviceId) {
+    public void onAdvertisementSent(int serviceId, int sentPacketCount) {
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) return;
 
@@ -848,9 +917,20 @@
             record.lastSentTimeMs = now;
             record.lastAdvertisedTimeMs = now;
         }
+        registration.sentPacketCount += sentPacketCount;
     }
 
     /**
+     * Called when {@link MdnsAdvertiser} sent a probing for the given service.
+     */
+    public void onProbingSent(int serviceId, int sentPacketCount) {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) return;
+        registration.sentPacketCount += sentPacketCount;
+    }
+
+
+    /**
      * Compute:
      * 2001:db8::1 --> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa
      *
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 16c7d27..71057fb 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -25,6 +25,7 @@
 
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -43,6 +44,9 @@
 public class MdnsReplySender {
     private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final int MSG_SEND = 1;
+    private static final int PACKET_NOT_SENT = 0;
+    private static final int PACKET_SENT = 1;
+
     @NonNull
     private final MdnsInterfaceSocket mSocket;
     @NonNull
@@ -78,44 +82,22 @@
      *
      * Must be called on the looper thread used by the {@link MdnsReplySender}.
      */
-    public void sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
+    public int sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
             throws IOException {
         ensureRunningOnHandlerThread(mHandler);
         if (!((destination.getAddress() instanceof Inet6Address && mSocket.hasJoinedIpv6())
                 || (destination.getAddress() instanceof Inet4Address && mSocket.hasJoinedIpv4()))) {
             // Skip sending if the socket has not joined the v4/v6 group (there was no address)
-            return;
+            return PACKET_NOT_SENT;
         }
+        final byte[] outBuffer = MdnsUtils.createRawDnsPacket(mPacketCreationBuffer, packet);
+        mSocket.send(new DatagramPacket(outBuffer, 0, outBuffer.length, destination));
+        return PACKET_SENT;
+    }
 
-        // TODO: support packets over size (send in multiple packets with TC bit set)
-        final MdnsPacketWriter writer = new MdnsPacketWriter(mPacketCreationBuffer);
-
-        writer.writeUInt16(0); // Transaction ID (advertisement: 0)
-        writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
-        writer.writeUInt16(packet.questions.size()); // questions count
-        writer.writeUInt16(packet.answers.size()); // answers count
-        writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
-        writer.writeUInt16(packet.additionalRecords.size()); // additional records count
-
-        for (MdnsRecord record : packet.questions) {
-            // Questions do not have TTL or data
-            record.writeHeaderFields(writer);
-        }
-        for (MdnsRecord record : packet.answers) {
-            record.write(writer, 0L);
-        }
-        for (MdnsRecord record : packet.authorityRecords) {
-            record.write(writer, 0L);
-        }
-        for (MdnsRecord record : packet.additionalRecords) {
-            record.write(writer, 0L);
-        }
-
-        final int len = writer.getWritePosition();
-        final byte[] outBuffer = new byte[len];
-        System.arraycopy(mPacketCreationBuffer, 0, outBuffer, 0, len);
-
-        mSocket.send(new DatagramPacket(outBuffer, 0, len, destination));
+    /** Get the packetCreationBuffer */
+    public byte[] getPacketCreationBuffer() {
+        return mPacketCreationBuffer;
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index df3bde8..c1c9c42 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -24,8 +24,11 @@
 import android.util.ArraySet;
 
 import com.android.server.connectivity.mdns.MdnsConstants;
+import com.android.server.connectivity.mdns.MdnsPacket;
+import com.android.server.connectivity.mdns.MdnsPacketWriter;
 import com.android.server.connectivity.mdns.MdnsRecord;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
@@ -165,6 +168,41 @@
     }
 
     /**
+     * Create a raw DNS packet.
+     */
+    public static byte[] createRawDnsPacket(@NonNull byte[] packetCreationBuffer,
+            @NonNull MdnsPacket packet) throws IOException {
+        // TODO: support packets over size (send in multiple packets with TC bit set)
+        final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
+
+        writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+        writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
+        writer.writeUInt16(packet.questions.size()); // questions count
+        writer.writeUInt16(packet.answers.size()); // answers count
+        writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
+        writer.writeUInt16(packet.additionalRecords.size()); // additional records count
+
+        for (MdnsRecord record : packet.questions) {
+            // Questions do not have TTL or data
+            record.writeHeaderFields(writer);
+        }
+        for (MdnsRecord record : packet.answers) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.authorityRecords) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.additionalRecords) {
+            record.write(writer, 0L);
+        }
+
+        final int len = writer.getWritePosition();
+        final byte[] outBuffer = new byte[len];
+        System.arraycopy(packetCreationBuffer, 0, outBuffer, 0, len);
+        return outBuffer;
+    }
+
+    /**
      * Checks if the MdnsRecord needs to be renewed or not.
      *
      * <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one,
diff --git a/service/Android.bp b/service/Android.bp
index 9ae3d6c..8e59e86 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -19,6 +19,12 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+service_remoteauth_pre_jarjar_lib = "service-remoteauth-pre-jarjar"
+
+// The above variables may have different values
+// depending on the branch, and this comment helps
+// separate them from the rest of the file to avoid merge conflicts
+
 aidl_interface {
     name: "connectivity_native_aidl_interface",
     local_include_dir: "binder",
@@ -236,7 +242,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
-        "service-remoteauth-pre-jarjar",
+        service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
     ],
     // The below libraries are not actually needed to build since no source is compiled
@@ -361,7 +367,7 @@
 java_genrule {
     name: "service-remoteauth-jarjar-gen",
     tool_files: [
-        ":service-remoteauth-pre-jarjar{.jar}",
+        ":" + service_remoteauth_pre_jarjar_lib + "{.jar}",
         "jarjar-excludes.txt",
     ],
     tools: [
@@ -369,7 +375,7 @@
     ],
     out: ["service_remoteauth_jarjar_rules.txt"],
     cmd: "$(location jarjar-rules-generator) " +
-        "$(location :service-remoteauth-pre-jarjar{.jar}) " +
+        "$(location :" + service_remoteauth_pre_jarjar_lib + "{.jar}) " +
         "--prefix com.android.server.remoteauth " +
         "--excludes $(location jarjar-excludes.txt) " +
         "--output $(out)",
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index 9ced44e..50a0635 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -187,15 +187,6 @@
     mTc.setPermissionForUids(permission, data);
 }
 
-static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
-    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
-    if (fd < 0) {
-        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
-        return;
-    }
-    mTc.dump(fd, verbose);
-}
-
 static jint native_synchronizeKernelRCU(JNIEnv* env, jobject self) {
     return -bpf::synchronizeKernelRCU();
 }
@@ -232,8 +223,6 @@
     (void*)native_swapActiveStatsMap},
     {"native_setPermissionForUids", "(I[I)V",
     (void*)native_setPermissionForUids},
-    {"native_dump", "(Ljava/io/FileDescriptor;Z)V",
-    (void*)native_dump},
     {"native_synchronizeKernelRCU", "()I",
     (void*)native_synchronizeKernelRCU},
 };
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 3828389..543bdc8 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -576,13 +576,5 @@
     }
 }
 
-void TrafficController::dump(int fd, bool verbose __unused) {
-    std::lock_guard guard(mMutex);
-    DumpWriter dw(fd);
-
-    ScopedIndent indentTop(dw);
-    dw.println("TrafficController");
-}
-
 }  // namespace net
 }  // namespace android
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index d610d25..86cf50a 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -22,7 +22,6 @@
 #include "android-base/thread_annotations.h"
 #include "bpf/BpfMap.h"
 #include "netd.h"
-#include "netdutils/DumpWriter.h"
 #include "netdutils/NetlinkListener.h"
 #include "netdutils/StatusOr.h"
 
@@ -55,8 +54,6 @@
     netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
                                           FirewallType type) EXCLUDES(mMutex);
 
-    void dump(int fd, bool verbose) EXCLUDES(mMutex);
-
     netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
             EXCLUDES(mMutex);
 
diff --git a/service/src/com/android/metrics/stats.proto b/service/src/com/android/metrics/stats.proto
index 99afb90..ecc0377 100644
--- a/service/src/com/android/metrics/stats.proto
+++ b/service/src/com/android/metrics/stats.proto
@@ -64,6 +64,18 @@
 
   // Record sent query count before stopped discovery
   optional int32 sent_query_count = 12;
+
+  // Record sent packet count before unregistered service
+  optional int32 sent_packet_count = 13;
+
+  // Record number of conflict during probing
+  optional int32 conflict_during_probing_count = 14;
+
+  // Record number of conflict after probing
+  optional int32 conflict_after_probing_count = 15;
+
+  // The random number between 0 ~ 999 for sampling
+  optional int32 random_number = 16;
 }
 
 /**
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 2842cc3..4b24aaf 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,6 +16,18 @@
 
 package com.android.server;
 
+import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.COOKIE_TAG_MAP_PATH;
+import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
+import static android.net.BpfNetMapsUtils.matchToString;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
@@ -107,16 +119,6 @@
     // BpfNetMaps is an only writer of this entry.
     private static final Object sCurrentStatsMapConfigLock = new Object();
 
-    private static final String CONFIGURATION_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
-    private static final String UID_OWNER_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
-    private static final String UID_PERMISSION_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
-    private static final String COOKIE_TAG_MAP_PATH =
-            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
-    private static final S32 UID_RULES_CONFIGURATION_KEY = new S32(0);
-    private static final S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new S32(1);
     private static final long UID_RULES_DEFAULT_CONFIGURATION = 0;
     private static final long STATS_SELECT_MAP_A = 0;
     private static final long STATS_SELECT_MAP_B = 1;
@@ -127,40 +129,10 @@
     private static IBpfMap<S32, U8> sUidPermissionMap = null;
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
 
-    // LINT.IfChange(match_type)
-    @VisibleForTesting public static final long NO_MATCH = 0;
-    @VisibleForTesting public static final long HAPPY_BOX_MATCH = (1 << 0);
-    @VisibleForTesting public static final long PENALTY_BOX_MATCH = (1 << 1);
-    @VisibleForTesting public static final long DOZABLE_MATCH = (1 << 2);
-    @VisibleForTesting public static final long STANDBY_MATCH = (1 << 3);
-    @VisibleForTesting public static final long POWERSAVE_MATCH = (1 << 4);
-    @VisibleForTesting public static final long RESTRICTED_MATCH = (1 << 5);
-    @VisibleForTesting public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
-    @VisibleForTesting public static final long IIF_MATCH = (1 << 7);
-    @VisibleForTesting public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
-    @VisibleForTesting public static final long OEM_DENY_1_MATCH = (1 << 9);
-    @VisibleForTesting public static final long OEM_DENY_2_MATCH = (1 << 10);
-    @VisibleForTesting public static final long OEM_DENY_3_MATCH = (1 << 11);
-    // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
-
     private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
             Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
             Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
     );
-    private static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
-            Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
-            Pair.create(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH"),
-            Pair.create(DOZABLE_MATCH, "DOZABLE_MATCH"),
-            Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
-            Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
-            Pair.create(RESTRICTED_MATCH, "RESTRICTED_MATCH"),
-            Pair.create(LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"),
-            Pair.create(IIF_MATCH, "IIF_MATCH"),
-            Pair.create(LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"),
-            Pair.create(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"),
-            Pair.create(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"),
-            Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH")
-    );
 
     /**
      * Set sEnableJavaBpfMap for test.
@@ -323,13 +295,6 @@
             return ConnectivityStatsLog.buildStatsEvent(NETWORK_BPF_MAP_INFO, cookieTagMapSize,
                     uidOwnerMapSize, uidPermissionMapSize);
         }
-
-        /**
-         * Call native_dump
-         */
-        public void nativeDump(final FileDescriptor fd, final boolean verbose) {
-            native_dump(fd, verbose);
-        }
     }
 
     /** Constructor used after T that doesn't need to use netd anymore. */
@@ -353,33 +318,6 @@
     }
 
     /**
-     * Get corresponding match from firewall chain.
-     */
-    @VisibleForTesting
-    public long getMatchByFirewallChain(final int chain) {
-        switch (chain) {
-            case FIREWALL_CHAIN_DOZABLE:
-                return DOZABLE_MATCH;
-            case FIREWALL_CHAIN_STANDBY:
-                return STANDBY_MATCH;
-            case FIREWALL_CHAIN_POWERSAVE:
-                return POWERSAVE_MATCH;
-            case FIREWALL_CHAIN_RESTRICTED:
-                return RESTRICTED_MATCH;
-            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
-                return LOW_POWER_STANDBY_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_1:
-                return OEM_DENY_1_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_2:
-                return OEM_DENY_2_MATCH;
-            case FIREWALL_CHAIN_OEM_DENY_3:
-                return OEM_DENY_3_MATCH;
-            default:
-                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
-        }
-    }
-
-    /**
      * Get if the chain is allow list or not.
      *
      * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
@@ -1049,26 +987,6 @@
         return sj.toString();
     }
 
-    private String matchToString(long matchMask) {
-        if (matchMask == NO_MATCH) {
-            return "NO_MATCH";
-        }
-
-        final StringJoiner sj = new StringJoiner(" ");
-        for (Pair<Long, String> match: MATCH_LIST) {
-            final long matchFlag = match.first;
-            final String matchName = match.second;
-            if ((matchMask & matchFlag) != 0) {
-                sj.add(matchName);
-                matchMask &= ~matchFlag;
-            }
-        }
-        if (matchMask != 0) {
-            sj.add("UNKNOWN_MATCH(" + matchMask + ")");
-        }
-        return sj.toString();
-    }
-
     private void dumpOwnerMatchConfig(final IndentingPrintWriter pw) {
         try {
             final long match = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
@@ -1105,7 +1023,8 @@
                     EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
                     + " devices, use dumpsys netd trafficcontroller instead.");
         }
-        mDeps.nativeDump(fd, verbose);
+
+        pw.println("TrafficController");  // required by CTS testDumpBpfNetMaps
 
         pw.println();
         pw.println("sEnableJavaBpfMap: " + sEnableJavaBpfMap);
@@ -1181,8 +1100,5 @@
     private native void native_setPermissionForUids(int permissions, int[] uids);
 
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    private static native void native_dump(FileDescriptor fd, boolean verbose);
-
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static native int native_synchronizeKernelRCU();
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 04d0b93..60523dd 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -934,6 +934,15 @@
     private final Map<String, ApplicationSelfCertifiedNetworkCapabilities>
             mSelfCertifiedCapabilityCache = new HashMap<>();
 
+    // Flag to enable the feature of closing frozen app sockets.
+    private final boolean mDestroyFrozenSockets;
+
+    // Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
+    private final boolean mDelayDestroyFrozenSockets;
+
+    // Uids that ConnectivityService is pending to close sockets of.
+    private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
+
     /**
      * Implements support for the legacy "one network per network type" model.
      *
@@ -1772,8 +1781,11 @@
             mCdmps = null;
         }
 
-        if (mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION)) {
+        mDestroyFrozenSockets = mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
+        mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+        if (mDestroyFrozenSockets) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
                 @Override
@@ -2983,26 +2995,109 @@
         }
     }
 
+    /**
+     * Check if the cell network is idle.
+     * @return true if the cell network state is idle
+     *         false if the cell network state is active or unknown
+     */
+    private boolean isCellNetworkIdle() {
+        final NetworkAgentInfo defaultNai = getDefaultNetwork();
+        if (defaultNai == null
+                || !defaultNai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            // mNetworkActivityTracker only tracks the activity of the default network. So if the
+            // cell network is not the default network, cell network state is unknown.
+            // TODO(b/279380356): Track cell network state when the cell is not the default network
+            return false;
+        }
+
+        return !mNetworkActivityTracker.isDefaultNetworkActive();
+    }
+
     private void handleFrozenUids(int[] uids, int[] frozenStates) {
         final ArraySet<Integer> ownerUids = new ArraySet<>();
 
         for (int i = 0; i < uids.length; i++) {
             if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
                 ownerUids.add(uids[i]);
+            } else {
+                mPendingFrozenUids.remove(uids[i]);
             }
         }
 
-        if (!ownerUids.isEmpty()) {
+        if (ownerUids.isEmpty()) {
+            return;
+        }
+
+        if (mDelayDestroyFrozenSockets && isCellNetworkIdle()) {
+            // Delay closing sockets to avoid waking the cell modem up.
+            // Wi-Fi network state is not considered since waking Wi-Fi modem up is much cheaper
+            // than waking cell modem up.
+            mPendingFrozenUids.addAll(ownerUids);
+        } else {
             try {
                 mDeps.destroyLiveTcpSocketsByOwnerUids(ownerUids);
-            } catch (Exception e) {
+            } catch (SocketException | InterruptedIOException | ErrnoException e) {
                 loge("Exception in socket destroy: " + e);
             }
         }
     }
 
+    private void closePendingFrozenSockets() {
+        ensureRunningOnConnectivityServiceThread();
+
+        try {
+            mDeps.destroyLiveTcpSocketsByOwnerUids(mPendingFrozenUids);
+        } catch (SocketException | InterruptedIOException | ErrnoException e) {
+            loge("Failed to close pending frozen app sockets: " + e);
+        }
+        mPendingFrozenUids.clear();
+    }
+
+    private void handleReportNetworkActivity(final NetworkActivityParams params) {
+        mNetworkActivityTracker.handleReportNetworkActivity(params);
+
+        if (mDelayDestroyFrozenSockets
+                && params.isActive
+                && params.label == TRANSPORT_CELLULAR
+                && !mPendingFrozenUids.isEmpty()) {
+            closePendingFrozenSockets();
+        }
+    }
+
+    /**
+     * If the cellular network is no longer the default network, close pending frozen sockets.
+     *
+     * @param newNetwork new default network
+     * @param oldNetwork old default network
+     */
+    private void maybeClosePendingFrozenSockets(NetworkAgentInfo newNetwork,
+            NetworkAgentInfo oldNetwork) {
+        final boolean isOldNetworkCellular = oldNetwork != null
+                && oldNetwork.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+        final boolean isNewNetworkCellular = newNetwork != null
+                && newNetwork.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+
+        if (isOldNetworkCellular
+                && !isNewNetworkCellular
+                && !mPendingFrozenUids.isEmpty()) {
+            closePendingFrozenSockets();
+        }
+    }
+
+    private void dumpCloseFrozenAppSockets(IndentingPrintWriter pw) {
+        pw.println("CloseFrozenAppSockets:");
+        pw.increaseIndent();
+        pw.print("mDestroyFrozenSockets="); pw.println(mDestroyFrozenSockets);
+        pw.print("mDelayDestroyFrozenSockets="); pw.println(mDelayDestroyFrozenSockets);
+        pw.print("mPendingFrozenUids="); pw.println(mPendingFrozenUids);
+        pw.decreaseIndent();
+    }
+
     @VisibleForTesting
     static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
+    @VisibleForTesting
+    static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
+            "delay_destroy_frozen_sockets_version";
 
     private void enforceInternetPermission() {
         mContext.enforceCallingOrSelfPermission(
@@ -3605,6 +3700,9 @@
         dumpAvoidBadWifiSettings(pw);
 
         pw.println();
+        dumpCloseFrozenAppSockets(pw);
+
+        pw.println();
 
         if (!CollectionUtils.contains(args, SHORT_ARG)) {
             pw.println();
@@ -4671,6 +4769,7 @@
                     //  incorrect) behavior.
                     mNetworkActivityTracker.updateDataActivityTracking(
                             null /* newNetwork */, nai);
+                    maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
                     ensureNetworkTransitionWakelock(nai.toShortString());
                 }
             }
@@ -5877,7 +5976,7 @@
                 }
                 case EVENT_REPORT_NETWORK_ACTIVITY:
                     final NetworkActivityParams arg = (NetworkActivityParams) msg.obj;
-                    mNetworkActivityTracker.handleReportNetworkActivity(arg);
+                    handleReportNetworkActivity(arg);
                     break;
                 case EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED:
                     handleMobileDataPreferredUidsChanged();
@@ -9117,6 +9216,7 @@
             mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
         }
         mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+        maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
         mProxyTracker.setDefaultProxy(null != newDefaultNetwork
                 ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
         resetHttpProxyForNonDefaultNetwork(oldDefaultNetwork);
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index b89ab1f..3358fd7 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -48,7 +48,7 @@
     @BeforeClassWithInfo
     public static void setUpOnceBase(TestInformation testInfo) throws Exception {
         DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
-        String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT : TEST_APK;
+        String testApk = deviceSdkLevel.isDeviceAtLeastV() ? TEST_APK_NEXT : TEST_APK;
 
         uninstallPackage(testInfo, TEST_PKG, false);
         installPackage(testInfo, testApk);
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index ff06a90..aa90f5f 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -16,24 +16,33 @@
 
 package android.security.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.modules.utils.build.testing.DeviceSdkLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
-import java.lang.Integer;
-import java.lang.String;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.ArrayList;
 
 /**
  * Host-side tests for values in /proc/net.
  *
  * These tests analyze /proc/net to verify that certain networking properties are correct.
  */
-public class ProcNetTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest {
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ProcNetTest extends BaseHostJUnit4Test implements IBuildReceiver, IDeviceTest {
     private static final String SPI_TIMEOUT_SYSCTL = "/proc/sys/net/core/xfrm_acq_expires";
     private static final int MIN_ACQ_EXPIRES = 3600;
     // Global sysctls. Must be present and set to 1.
@@ -72,13 +81,12 @@
      */
     @Override
     public void setDevice(ITestDevice device) {
-        super.setDevice(device);
         mDevice = device;
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    /** Run before each test case. */
+    @Before
+    public void setUp() throws Exception {
         mSysctlDirs = getSysctlDirs();
     }
 
@@ -113,6 +121,7 @@
     /**
      * Checks that SPI default timeouts are overridden, and set to a reasonable length of time
      */
+    @Test
     public void testMinAcqExpires() throws Exception {
         int value = readIntFromPath(SPI_TIMEOUT_SYSCTL);
         assertAtLeast(SPI_TIMEOUT_SYSCTL, value, MIN_ACQ_EXPIRES);
@@ -122,6 +131,7 @@
      * Checks that the sysctls for multinetwork kernel features are present and
      * enabled.
      */
+    @Test
     public void testProcSysctls() throws Exception {
         for (String sysctl : GLOBAL_SYSCTLS) {
             int value = readIntFromPath(sysctl);
@@ -138,6 +148,7 @@
     /**
      * Verify that accept_ra_rt_info_{min,max}_plen exists and is set to the expected value
      */
+    @Test
     public void testAcceptRaRtInfoMinMaxPlen() throws Exception {
         for (String interfaceDir : mSysctlDirs) {
             String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_min_plen";
@@ -153,6 +164,7 @@
      * Verify that router_solicitations exists and is set to the expected value
      * and verify that router_solicitation_max_interval exists and is in an acceptable interval.
      */
+    @Test
     public void testRouterSolicitations() throws Exception {
         for (String interfaceDir : mSysctlDirs) {
             String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
@@ -172,7 +184,11 @@
      * (This repeats the VTS test, and is here for good performance of the internet as a whole.)
      * TODO: revisit this once a better CC algorithm like BBR2 is available.
      */
+    @Test
     public void testCongestionControl() throws Exception {
+        final DeviceSdkLevel dsl = new DeviceSdkLevel(mDevice);
+        assumeTrue(dsl.isDeviceAtLeastV());
+
         String path = "/proc/sys/net/ipv4/tcp_congestion_control";
         String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
         assertEquals(value, "cubic");
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 77cea1a..3a76cc2 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1132,7 +1132,6 @@
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testRegisterNetworkCallback_withPendingIntent() {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
@@ -1276,7 +1275,6 @@
 
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testRegisterNetworkRequest_identicalPendingIntents() throws Exception {
         runIdenticalPendingIntentsRequestTest(false /* useListen */);
     }
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 3146b41..b7e5205 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -385,6 +385,9 @@
         }
         registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         releaseTetheredInterface()
+        // Force releaseTetheredInterface() to be processed before starting the next test by calling
+        // setEthernetEnabled(true) which always waits on a callback.
+        setEthernetEnabled(true)
     }
 
     // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
@@ -635,6 +638,9 @@
             // do nothing -- the TimeoutException indicates that no interface is available for
             // tethering.
             releaseTetheredInterface()
+            // Force releaseTetheredInterface() to be processed before proceeding by calling
+            // setEthernetEnabled(true) which always waits on a callback.
+            setEthernetEnabled(true)
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 49a6ef1..17a135a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -163,6 +163,7 @@
 
     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+    private val serviceName2 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
     private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
     private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -890,14 +891,48 @@
         }
     }
 
-    fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo) {
-        assertEquals(serviceName, serviceInfo.key.serviceName)
-        assertEquals(serviceType, serviceInfo.key.serviceType)
-        assertEquals(listOf<String>("_subtype"), serviceInfo.subtypes)
+    fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo, si: NsdServiceInfo) {
+        val expectedServiceType = si.serviceType.split(",")[0]
+        assertEquals(si.serviceName, serviceInfo.key.serviceName)
+        assertEquals(expectedServiceType, serviceInfo.key.serviceType)
+        assertEquals(listOf("_subtype"), serviceInfo.subtypes)
         assertTrue(serviceInfo.hostname.startsWith("Android_"))
         assertTrue(serviceInfo.hostname.endsWith("local"))
         assertEquals(0, serviceInfo.priority)
         assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
+        val offloadPayload = serviceInfo.offloadPayload
+        assertNotNull(offloadPayload)
+        val dnsPacket = TestDnsPacket(offloadPayload)
+        assertEquals(0x8400, dnsPacket.header.flags)
+        assertEquals(0, dnsPacket.records[DnsPacket.QDSECTION].size)
+        assertTrue(dnsPacket.records[DnsPacket.ANSECTION].size >= 5)
+        assertEquals(0, dnsPacket.records[DnsPacket.NSSECTION].size)
+        assertEquals(0, dnsPacket.records[DnsPacket.ARSECTION].size)
+
+        val ptrRecord = dnsPacket.records[DnsPacket.ANSECTION][0]
+        assertEquals("$expectedServiceType.local", ptrRecord.dName)
+        assertEquals(0x0C /* PTR */, ptrRecord.nsType)
+        val ptrSubRecord = dnsPacket.records[DnsPacket.ANSECTION][1]
+        assertEquals("_subtype._sub.$expectedServiceType.local", ptrSubRecord.dName)
+        assertEquals(0x0C /* PTR */, ptrSubRecord.nsType)
+        val srvRecord = dnsPacket.records[DnsPacket.ANSECTION][2]
+        assertEquals("${si.serviceName}.$expectedServiceType.local", srvRecord.dName)
+        assertEquals(0x21 /* SRV */, srvRecord.nsType)
+        val txtRecord = dnsPacket.records[DnsPacket.ANSECTION][3]
+        assertEquals("${si.serviceName}.$expectedServiceType.local", txtRecord.dName)
+        assertEquals(0x10 /* TXT */, txtRecord.nsType)
+        val iface = NetworkInterface.getByName(testNetwork1.iface.interfaceName)
+        val allAddress = iface.inetAddresses.toList()
+        for (i in 4 until dnsPacket.records[DnsPacket.ANSECTION].size) {
+            val addressRecord = dnsPacket.records[DnsPacket.ANSECTION][i]
+            assertTrue(addressRecord.dName.startsWith("Android_"))
+            assertTrue(addressRecord.dName.endsWith("local"))
+            assertTrue(addressRecord.nsType in arrayOf(0x1C /* AAAA */, 0x01 /* A */))
+            val rData = addressRecord.rr
+            assertNotNull(rData)
+            val addr = InetAddress.getByAddress(rData)
+            assertTrue(addr in allAddress)
+        }
     }
 
     @Test
@@ -907,36 +942,63 @@
         // The offload callbacks are only supported with the new backend,
         // enabled with target SDK U+.
         assumeTrue(isAtLeastU() || targetSdkVersion > Build.VERSION_CODES.TIRAMISU)
+
+        // TODO: also have a test that use an executor that runs in a different thread, and pass
+        // in the thread ID NsdServiceInfo to check it
+        val si1 = NsdServiceInfo()
+        si1.serviceType = "$serviceType,_subtype"
+        si1.serviceName = serviceName
+        si1.network = testNetwork1.network
+        si1.port = 23456
+        val record1 = NsdRegistrationRecord()
+
+        val si2 = NsdServiceInfo()
+        si2.serviceType = "$serviceType,_subtype"
+        si2.serviceName = serviceName2
+        si2.network = testNetwork1.network
+        si2.port = 12345
+        val record2 = NsdRegistrationRecord()
         val offloadEngine = TestNsdOffloadEngine()
-        runAsShell(NETWORK_SETTINGS) {
-            nsdManager.registerOffloadEngine(testNetwork1.iface.interfaceName,
-                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(),
-                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK.toLong(),
-                { it.run() }, offloadEngine)
-        }
 
-        val si = NsdServiceInfo()
-        si.serviceType = "$serviceType,_subtype"
-        si.serviceName = serviceName
-        si.network = testNetwork1.network
-        si.port = 12345
-        val record = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, record)
-        val addOrUpdateEvent = offloadEngine
-            .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent> {
-                it.info.key.serviceName == serviceName
+        tryTest {
+            // Register service before the OffloadEngine is registered.
+            nsdManager.registerService(si1, NsdManager.PROTOCOL_DNS_SD, record1)
+            record1.expectCallback<ServiceRegistered>()
+            runAsShell(NETWORK_SETTINGS) {
+                nsdManager.registerOffloadEngine(testNetwork1.iface.interfaceName,
+                    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(),
+                    OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK.toLong(),
+                    { it.run() }, offloadEngine)
             }
-        checkOffloadServiceInfo(addOrUpdateEvent.info)
+            val addOrUpdateEvent1 = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent> {
+                    it.info.key.serviceName == si1.serviceName
+                }
+            checkOffloadServiceInfo(addOrUpdateEvent1.info, si1)
 
-        nsdManager.unregisterService(record)
-        val unregisterEvent = offloadEngine
-            .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.RemoveEvent> {
-                it.info.key.serviceName == serviceName
+            // Register service after OffloadEngine is registered.
+            nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, record2)
+            record2.expectCallback<ServiceRegistered>()
+            val addOrUpdateEvent2 = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent> {
+                    it.info.key.serviceName == si2.serviceName
+                }
+            checkOffloadServiceInfo(addOrUpdateEvent2.info, si2)
+
+            nsdManager.unregisterService(record2)
+            record2.expectCallback<ServiceUnregistered>()
+            val unregisterEvent = offloadEngine
+                .expectCallbackEventually<TestNsdOffloadEngine.OffloadEvent.RemoveEvent> {
+                    it.info.key.serviceName == si2.serviceName
+                }
+            checkOffloadServiceInfo(unregisterEvent.info, si2)
+        } cleanupStep {
+            runAsShell(NETWORK_SETTINGS) {
+                nsdManager.unregisterOffloadEngine(offloadEngine)
             }
-        checkOffloadServiceInfo(unregisterEvent.info)
-
-        runAsShell(NETWORK_SETTINGS) {
-            nsdManager.unregisterOffloadEngine(offloadEngine)
+        } cleanup {
+            nsdManager.unregisterService(record1)
+            record1.expectCallback<ServiceUnregistered>()
         }
     }
 
@@ -1381,6 +1443,11 @@
 ): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isReplyFor("$serviceName.$serviceType.local") }
 
 private class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+    val header: DnsHeader
+        get() = mHeader
+    val records: Array<List<DnsRecord>>
+        get() = mRecords
+
     fun isProbeFor(name: String): Boolean = mRecords[QDSECTION].any {
         it.dName == name && it.nsType == 0xff /* ANY */
     }
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index aa09b84..96330e2 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -65,6 +65,7 @@
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ConnectivitySettingsUtils;
 import com.android.testutils.ConnectUtil;
 
@@ -590,8 +591,12 @@
                         callback.waitForAvailable());
             }
 
-            runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabledForReason(
-                    TelephonyManager.DATA_ENABLED_REASON_USER, enabled));
+            if (SdkLevel.isAtLeastS()) {
+                runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabledForReason(
+                        TelephonyManager.DATA_ENABLED_REASON_USER, enabled));
+            } else {
+                runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabled(enabled));
+            }
             if (enabled) {
                 assertNotNull("Enabling mobile data did not connect mobile data",
                         callback.waitForAvailable());
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
index e326844..ad9a731 100644
--- a/tests/cts/netpermission/internetpermission/AndroidTest.xml
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -16,6 +16,7 @@
 <configuration description="Config for CTS internet permission test cases">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index a1019fa..fb6c814 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -16,6 +16,7 @@
 <configuration description="Config for CTS update stats permission test cases">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 442d69f..15263cc 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -95,6 +95,7 @@
     NETD "map_netd_cookie_tag_map",
     NETD "map_netd_iface_index_name_map",
     NETD "map_netd_iface_stats_map",
+    NETD "map_netd_ingress_discard_map",
     NETD "map_netd_stats_map_A",
     NETD "map_netd_stats_map_B",
     NETD "map_netd_uid_counterset_map",
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index 7f893df..a82e29b 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -21,12 +21,15 @@
 import android.stats.connectivity.NsdEventType
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import java.util.Random
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 
@@ -34,6 +37,12 @@
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class NetworkNsdReportedMetricsTest {
     private val deps = mock(NetworkNsdReportedMetrics.Dependencies::class.java)
+    private val random = mock(Random::class.java)
+
+    @Before
+    fun setUp() {
+        doReturn(random).`when`(deps).makeRandomGenerator()
+    }
 
     @Test
     fun testReportServiceRegistrationSucceeded() {
@@ -80,8 +89,13 @@
         val clientId = 99
         val transactionId = 100
         val durationMs = 10L
+        val repliedRequestsCount = 25
+        val sentPacketCount = 50
+        val conflictDuringProbingCount = 2
+        val conflictAfterProbingCount = 1
         val metrics = NetworkNsdReportedMetrics(true /* isLegacy */, clientId, deps)
-        metrics.reportServiceUnregistration(transactionId, durationMs)
+        metrics.reportServiceUnregistration(transactionId, durationMs, repliedRequestsCount,
+                sentPacketCount, conflictDuringProbingCount, conflictAfterProbingCount)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -92,6 +106,10 @@
             assertEquals(NsdEventType.NET_REGISTER, it.type)
             assertEquals(MdnsQueryResult.MQR_SERVICE_UNREGISTERED, it.queryResult)
             assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(repliedRequestsCount, it.repliedRequestsCount)
+            assertEquals(sentPacketCount, it.sentPacketCount)
+            assertEquals(conflictDuringProbingCount, it.conflictDuringProbingCount)
+            assertEquals(conflictAfterProbingCount, it.conflictAfterProbingCount)
         }
     }
 
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 19fa41d..5f280c6 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,6 +16,21 @@
 
 package com.android.server;
 
+import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
+import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
+import static android.net.BpfNetMapsConstants.NO_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
+import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
+import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
+import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
@@ -33,19 +48,6 @@
 import static android.system.OsConstants.EINVAL;
 import static android.system.OsConstants.EPERM;
 
-import static com.android.server.BpfNetMaps.DOZABLE_MATCH;
-import static com.android.server.BpfNetMaps.HAPPY_BOX_MATCH;
-import static com.android.server.BpfNetMaps.IIF_MATCH;
-import static com.android.server.BpfNetMaps.LOCKDOWN_VPN_MATCH;
-import static com.android.server.BpfNetMaps.LOW_POWER_STANDBY_MATCH;
-import static com.android.server.BpfNetMaps.NO_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_1_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_2_MATCH;
-import static com.android.server.BpfNetMaps.OEM_DENY_3_MATCH;
-import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
-import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
-import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
-import static com.android.server.BpfNetMaps.STANDBY_MATCH;
 import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
 
 import static org.junit.Assert.assertEquals;
@@ -62,6 +64,7 @@
 
 import android.app.StatsManager;
 import android.content.Context;
+import android.net.BpfNetMapsUtils;
 import android.net.INetd;
 import android.os.Build;
 import android.os.ServiceSpecificException;
@@ -112,8 +115,6 @@
     private static final int NO_IIF = 0;
     private static final int NULL_IIF = 0;
     private static final String CHAINNAME = "fw_dozable";
-    private static final S32 UID_RULES_CONFIGURATION_KEY = new S32(0);
-    private static final S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new S32(1);
     private static final List<Integer> FIREWALL_CHAINS = List.of(
             FIREWALL_CHAIN_DOZABLE,
             FIREWALL_CHAIN_STANDBY,
@@ -170,7 +171,7 @@
     private long getMatch(final List<Integer> chains) {
         long match = 0;
         for (final int chain: chains) {
-            match |= mBpfNetMaps.getMatchByFirewallChain(chain);
+            match |= BpfNetMapsUtils.getMatchByFirewallChain(chain);
         }
         return match;
     }
@@ -239,7 +240,7 @@
     private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
         long expectedMatch = 0;
         for (final int chain: testChains) {
-            expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+            expectedMatch |= BpfNetMapsUtils.getMatchByFirewallChain(chain);
         }
 
         assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 708697c..e5dec56 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -154,6 +154,7 @@
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
 
+import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -214,6 +215,7 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -424,7 +426,6 @@
 import com.android.testutils.FunctionalUtils.ThrowingRunnable;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestableNetworkCallback;
 import com.android.testutils.TestableNetworkOfferCallback;
 
@@ -2131,6 +2132,8 @@
                     return true;
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
+                case DELAY_DESTROY_FROZEN_SOCKETS_VERSION:
+                    return true;
                 default:
                     return super.isFeatureEnabled(context, name);
             }
@@ -2281,7 +2284,9 @@
         @Override @SuppressWarnings("DirectInvocationOnMock")
         public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids) {
             // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
-            mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+            // Create copy of ownerUids so that tests can verify the correct value even if the
+            // ConnectivityService update the ownerUids after this method call.
+            mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(new ArraySet<>(ownerUids));
         }
 
         final ArrayTrackRecord<Pair<Integer, Long>>.ReadHead mScheduledEvaluationTimeouts =
@@ -7430,7 +7435,6 @@
         assertPinnedToWifiWithCellDefault();
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testNetworkCallbackMaximum() throws Exception {
         final int MAX_REQUESTS = 100;
@@ -7549,6 +7553,19 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.requestNetwork(networkRequest, networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // While requestNetwork increases the count synchronously, unregister decreases it
+            // asynchronously on a handler, so unregistering doesn't immediately free up
+            // a slot : calling unregister-register when max requests are registered throws.
+            // Potential fix : ConnectivityService catches TooManyRequestsException once when
+            // creating NetworkRequestInfo and waits for handler thread (see
+            // https://r.android.com/2707373 for impl). However, this complexity is not equal to
+            // the issue ; the purpose of having "max requests" is only to help apps detect leaks.
+            // Apps relying on exact enforcement or rapid request registration should reconsider.
+            //
+            // In this test, test thread registering all before handler thread decrements can cause
+            // flakes. A single waitForIdle at (e.g.) MAX_REQUESTS / 2 processes decrements up to
+            // that point, fixing the flake.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7556,6 +7573,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerNetworkCallback(networkRequest, networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7563,6 +7582,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerDefaultNetworkCallback(networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7570,6 +7591,8 @@
             NetworkCallback networkCallback = new NetworkCallback();
             mCm.registerDefaultNetworkCallback(networkCallback);
             mCm.unregisterNetworkCallback(networkCallback);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7579,6 +7602,8 @@
                 mCm.registerDefaultNetworkCallbackForUid(1000000 + i, networkCallback,
                         new Handler(ConnectivityThread.getInstanceLooper()));
                 mCm.unregisterNetworkCallback(networkCallback);
+                // See comment above for the reasons for this wait.
+                if (MAX_REQUESTS / 2 == i) waitForIdle();
             }
         });
         waitForIdle();
@@ -7588,6 +7613,8 @@
                     mContext, 0 /* requestCode */, new Intent("e" + i), FLAG_IMMUTABLE);
             mCm.requestNetwork(networkRequest, pendingIntent);
             mCm.unregisterNetworkCallback(pendingIntent);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
         waitForIdle();
 
@@ -7596,6 +7623,8 @@
                     mContext, 0 /* requestCode */, new Intent("f" + i), FLAG_IMMUTABLE);
             mCm.registerNetworkCallback(networkRequest, pendingIntent);
             mCm.unregisterNetworkCallback(pendingIntent);
+            // See comment above for the reasons for this wait.
+            if (MAX_REQUESTS / 2 == i) waitForIdle();
         }
     }
 
@@ -11267,6 +11296,9 @@
     }
 
     private void doTestInterfaceClassActivityChanged(final int transportType) throws Exception {
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                getRegisteredNetdUnsolicitedEventListener();
+
         final int legacyType = transportToLegacyType(transportType);
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(transportToTestIfaceName(transportType));
@@ -11283,12 +11315,8 @@
 
             mCm.addDefaultNetworkActiveListener(listener);
 
-            ArgumentCaptor<BaseNetdUnsolicitedEventListener> netdCallbackCaptor =
-                    ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
-            verify(mMockNetd).registerUnsolicitedEventListener(netdCallbackCaptor.capture());
-
             // Interface goes to inactive state
-            netdCallbackCaptor.getValue().onInterfaceClassActivityChanged(false /* isActive */,
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
                     transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
             mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
                     TIMESTAMP);
@@ -11296,7 +11324,7 @@
             assertFalse(mCm.isDefaultNetworkActive());
 
             // Interface goes to active state
-            netdCallbackCaptor.getValue().onInterfaceClassActivityChanged(true /* isActive */,
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
                     transportType, TIMESTAMP, TEST_PACKAGE_UID);
             mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
             assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
@@ -16390,6 +16418,15 @@
         // Other callbacks will be unregistered by tearDown()
     }
 
+    private NetworkCallback requestForEnterpriseId(@NetworkCapabilities.EnterpriseId final int id) {
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_ENTERPRISE).addEnterpriseId(id).build();
+        final NetworkRequest req = new NetworkRequest.Builder().setCapabilities(nc).build();
+        final NetworkCallback cb = new TestableNetworkCallback();
+        mCm.requestNetwork(req, cb);
+        return cb;
+    }
+
     /**
      * Make sure per profile network preferences behave as expected when multiple slices with
      * multiple different apps within same user profile is configured.
@@ -16397,8 +16434,6 @@
     @Test
     public void testSetPreferenceWithMultiplePreferences()
             throws Exception {
-        final InOrder inOrder = inOrder(mMockNetd);
-
         final UserHandle testHandle = setupEnterpriseNetwork();
         mServiceContext.setWorkProfile(testHandle, true);
         registerDefaultNetworkCallbacks();
@@ -16436,6 +16471,12 @@
         final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
         final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
 
+        final NetworkCallback keepupCb1 = requestForEnterpriseId(NET_ENTERPRISE_ID_1);
+        final NetworkCallback keepupCb2 = requestForEnterpriseId(NET_ENTERPRISE_ID_2);
+        final NetworkCallback keepupCb3 = requestForEnterpriseId(NET_ENTERPRISE_ID_3);
+        final NetworkCallback keepupCb4 = requestForEnterpriseId(NET_ENTERPRISE_ID_4);
+        final NetworkCallback keepupCb5 = requestForEnterpriseId(NET_ENTERPRISE_ID_5);
+
         workAgent1.connect(true);
         workAgent2.connect(true);
         workAgent3.connect(true);
@@ -16594,6 +16635,12 @@
         appCb4.expectAvailableCallbacksValidated(mCellAgent);
         mCellAgent.disconnect();
 
+        mCm.unregisterNetworkCallback(keepupCb1);
+        mCm.unregisterNetworkCallback(keepupCb2);
+        mCm.unregisterNetworkCallback(keepupCb3);
+        mCm.unregisterNetworkCallback(keepupCb4);
+        mCm.unregisterNetworkCallback(keepupCb5);
+
         mCm.unregisterNetworkCallback(appCb1);
         mCm.unregisterNetworkCallback(appCb2);
         mCm.unregisterNetworkCallback(appCb3);
@@ -18524,6 +18571,27 @@
                 anyInt());
     }
 
+    // UidFrozenStateChangedCallback is added in U API.
+    // Returning UidFrozenStateChangedCallback directly makes the test fail on T- devices since
+    // AndroidJUnit4ClassRunner iterates all declared methods and tries to resolve the return type.
+    // Solve this by wrapping it in an AtomicReference. Because of erasure, this removes the
+    // resolving problem as the type isn't seen dynamically.
+    private AtomicReference<UidFrozenStateChangedCallback> getUidFrozenStateChangedCallback() {
+        ArgumentCaptor<UidFrozenStateChangedCallback> activityManagerCallbackCaptor =
+                ArgumentCaptor.forClass(UidFrozenStateChangedCallback.class);
+        verify(mActivityManager).registerUidFrozenStateChangedCallback(any(),
+                activityManagerCallbackCaptor.capture());
+        return new AtomicReference<>(activityManagerCallbackCaptor.getValue());
+    }
+
+    private BaseNetdUnsolicitedEventListener getRegisteredNetdUnsolicitedEventListener()
+            throws RemoteException {
+        ArgumentCaptor<BaseNetdUnsolicitedEventListener> netdCallbackCaptor =
+                ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
+        verify(mMockNetd).registerUnsolicitedEventListener(netdCallbackCaptor.capture());
+        return netdCallbackCaptor.getValue();
+    }
+
     private static final int TEST_FROZEN_UID = 1000;
     private static final int TEST_UNFROZEN_UID = 2000;
 
@@ -18534,22 +18602,177 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testFrozenUidSocketDestroy() throws Exception {
-        ArgumentCaptor<UidFrozenStateChangedCallback> callbackArg =
-                ArgumentCaptor.forClass(UidFrozenStateChangedCallback.class);
-
-        verify(mActivityManager).registerUidFrozenStateChangedCallback(any(),
-                callbackArg.capture());
+        final UidFrozenStateChangedCallback callback =
+                getUidFrozenStateChangedCallback().get();
 
         final int[] uids = {TEST_FROZEN_UID, TEST_UNFROZEN_UID};
         final int[] frozenStates = {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN};
 
-        callbackArg.getValue().onUidFrozenStateChanged(uids, frozenStates);
+        callback.onUidFrozenStateChanged(uids, frozenStates);
 
         waitForIdle();
 
         verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(Set.of(TEST_FROZEN_UID));
     }
 
+    private void doTestDelayFrozenUidSocketDestroy(int transportType,
+            boolean freezeWithNetworkInactive, boolean expectDelay) throws Exception {
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(transportToTestIfaceName(transportType));
+        final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+        testAndCleanup(() -> {
+            final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
+                    getUidFrozenStateChangedCallback().get();
+            final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                    getRegisteredNetdUnsolicitedEventListener();
+
+            mCm.registerDefaultNetworkCallback(defaultCallback);
+            agent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+            if (freezeWithNetworkInactive) {
+                // Make network inactive
+                netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                        transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+            }
+
+            // Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
+            final int[] uids1 = {TEST_FROZEN_UID, TEST_UNFROZEN_UID};
+            final int[] frozenStates1 = {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_FROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids1, frozenStates1);
+            waitForIdle();
+
+            if (expectDelay) {
+                verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+            } else {
+                verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(
+                        Set.of(TEST_FROZEN_UID, TEST_UNFROZEN_UID));
+                clearInvocations(mDestroySocketsWrapper);
+            }
+
+            // Unfreeze TEST_UNFROZEN_UID
+            final int[] uids2 = {TEST_UNFROZEN_UID};
+            final int[] frozenStates2 = {UID_FROZEN_STATE_UNFROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids2, frozenStates2);
+
+            // Make network active
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+                    transportType, TIMESTAMP, TEST_PACKAGE_UID);
+            waitForIdle();
+
+            if (expectDelay) {
+                verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(
+                        Set.of(TEST_FROZEN_UID));
+            } else {
+                verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+            }
+        }, () -> { // Cleanup
+                agent.disconnect();
+            }, () -> {
+                mCm.unregisterNetworkCallback(defaultCallback);
+            });
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
+                false /* freezeWithNetworkInactive */, false /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
+        // When the default network is cellular and cellular network is inactive, closing socket
+        // is delayed.
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
+                true /* freezeWithNetworkInactive */, true /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
+                false /* freezeWithNetworkInactive */, false /* expectDelay */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
+        doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
+                true /* freezeWithNetworkInactive */, false /* expectDelay */);
+    }
+
+    /**
+     * @param switchToWifi if true, simulate a migration of the default network to wifi
+     *                     if false, simulate a cell disconnection
+     */
+    private void doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(final boolean switchToWifi)
+            throws Exception {
+        final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
+                getUidFrozenStateChangedCallback().get();
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedEventListener =
+                getRegisteredNetdUnsolicitedEventListener();
+
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+        try {
+            mCellAgent.connect(true);
+            defaultCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
+
+            // Make cell network inactive
+            netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+                    TRANSPORT_CELLULAR, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+
+            // Freeze TEST_FROZEN_UID
+            final int[] uids = {TEST_FROZEN_UID};
+            final int[] frozenStates = {UID_FROZEN_STATE_FROZEN};
+            uidFrozenStateChangedCallback.onUidFrozenStateChanged(uids, frozenStates);
+            waitForIdle();
+
+            // Closing frozen sockets should be delayed since the default network is cellular
+            // and cellular network is inactive.
+            verify(mDestroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(any());
+
+            if (switchToWifi) {
+                mWiFiAgent.connect(true);
+                defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiAgent);
+            } else {
+                mCellAgent.disconnect();
+                waitForIdle();
+            }
+
+            // Pending frozen sockets should be closed since the cellular network is no longer the
+            // default network.
+            verify(mDestroySocketsWrapper)
+                    .destroyLiveTcpSocketsByOwnerUids(Set.of(TEST_FROZEN_UID));
+        } finally {
+            mCm.unregisterNetworkCallback(defaultCallback);
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testLoseCellDefaultNetwork_SwitchToWifi_ClosePendingFrozenSockets()
+            throws Exception {
+        doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(true /* switchToWifi */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testLoseCellDefaultNetwork_NoDefaultNetwork_ClosePendingFrozenSockets()
+            throws Exception {
+        doTestLoseCellDefaultNetwork_ClosePendingFrozenSockets(false /* switchToWifi */);
+    }
+
     @Test
     public void testDisconnectSuspendedNetworkStopClatd() throws Exception {
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index f0c7dcc..44ed02a 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,20 +16,27 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.content.pm.PermissionInfo.PROTECTION_SIGNATURE;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND;
 import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
 import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
 import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
 
+import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
 import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
 import static com.android.server.NsdService.MdnsListener;
 import static com.android.server.NsdService.NO_TRANSACTION;
@@ -68,6 +75,8 @@
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
 import android.net.INetd;
 import android.net.Network;
 import android.net.mdns.aidl.DiscoveryInfo;
@@ -84,6 +93,7 @@
 import android.net.nsd.NsdManager.ResolveListener;
 import android.net.nsd.NsdManager.ServiceInfoCallback;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
@@ -161,6 +171,7 @@
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
     @Mock Context mContext;
+    @Mock PackageManager mPackageManager;
     @Mock ContentResolver mResolver;
     @Mock MDnsManager mMockMDnsM;
     @Mock Dependencies mDeps;
@@ -198,6 +209,7 @@
         mockService(mContext, MDnsManager.class, MDnsManager.MDNS_SERVICE, mMockMDnsM);
         mockService(mContext, WifiManager.class, Context.WIFI_SERVICE, mWifiManager);
         mockService(mContext, ActivityManager.class, Context.ACTIVITY_SERVICE, mActivityManager);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
         if (mContext.getSystemService(MDnsManager.class) == null) {
             // Test is using mockito-extended
             doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
@@ -1210,6 +1222,8 @@
         verify(mMockMDnsM).stopOperation(legacyIdCaptor.getValue());
         verify(mAdvertiser, never()).removeService(anyInt());
 
+        doReturn(mock(MdnsAdvertiser.AdvertiserMetrics.class))
+                .when(mAdvertiser).getAdvertiserMetrics(anyInt());
         client.unregisterService(regListenerWithFeature);
         waitForIdle();
         verify(mAdvertiser).removeService(serviceIdCaptor.getValue());
@@ -1300,14 +1314,20 @@
                 new NsdServiceInfo(regInfo.getServiceName(), null))));
         verify(mMetrics).reportServiceRegistrationSucceeded(regId, 10L /* durationMs */);
 
+        final MdnsAdvertiser.AdvertiserMetrics metrics = new MdnsAdvertiser.AdvertiserMetrics(
+                50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
         doReturn(TEST_TIME_MS + 100L).when(mClock).elapsedRealtime();
+        doReturn(metrics).when(mAdvertiser).getAdvertiserMetrics(regId);
         client.unregisterService(regListener);
         waitForIdle();
         verify(mAdvertiser).removeService(idCaptor.getValue());
         verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
                 argThat(info -> matches(info, regInfo)));
         verify(mSocketProvider, timeout(TIMEOUT_MS)).requestStopWhenInactive();
-        verify(mMetrics).reportServiceUnregistration(regId, 100L /* durationMs */);
+        verify(mMetrics).reportServiceUnregistration(regId, 100L /* durationMs */,
+                50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
     }
 
     @Test
@@ -1664,6 +1684,33 @@
         assertThrows(IllegalArgumentException.class, () -> new NsdManager(mContext, service));
     }
 
+    @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    public void testRegisterOffloadEngine_checkPermission()
+            throws PackageManager.NameNotFoundException {
+        final NsdManager client = connectClient(mService);
+        final OffloadEngine offloadEngine = mock(OffloadEngine.class);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                PERMISSION_MAINLINE_NETWORK_STACK);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(NETWORK_SETTINGS);
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                REGISTER_NSD_OFFLOAD_ENGINE);
+
+        PermissionInfo permissionInfo = new PermissionInfo("");
+        permissionInfo.packageName = "android";
+        permissionInfo.protectionLevel = PROTECTION_SIGNATURE;
+        doReturn(permissionInfo).when(mPackageManager).getPermissionInfo(
+                REGISTER_NSD_OFFLOAD_ENGINE, 0);
+        client.registerOffloadEngine("iface1", OffloadEngine.OFFLOAD_TYPE_REPLY,
+                OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK,
+                Runnable::run, offloadEngine);
+        client.unregisterOffloadEngine(offloadEngine);
+
+        // TODO: add checks to test the packageName other than android
+    }
+
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 385f831..56346ad 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -57,6 +57,7 @@
 import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
 import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
 import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -2040,6 +2041,8 @@
 
         // Check if allowBypass is set or not.
         assertTrue(nacCaptor.getValue().isBypassableVpn());
+        // Check if extra info for VPN is set.
+        assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG));
         final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
         assertTrue(info.isBypassable());
         assertEquals(areLongLivedTcpConnectionsExpensive,
@@ -2500,6 +2503,40 @@
     }
 
     @Test
+    public void testStartPlatformVpn_underlyingNetworkNotChange() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+        // Trigger update on the same network should not cause underlying network change in NC of
+        // the VPN network
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK);
+        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK,
+                new NetworkCapabilities.Builder()
+                        .setSubscriptionIds(Set.of(TEST_SUB_ID))
+                        .build());
+        // Verify setNetwork() called but no underlying network update
+        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK),
+                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
+                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
+                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
+        verify(mMockNetworkAgent, never())
+                .doSetUnderlyingNetworks(any());
+
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
+                new NetworkCapabilities.Builder().build());
+
+        // A new network should trigger both setNetwork() and a underlying network update.
+        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
+                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
+                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
+                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
+        verify(mMockNetworkAgent).doSetUnderlyingNetworks(
+                Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
     public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
         final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
                 createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
@@ -2523,6 +2560,12 @@
                 eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
                 eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
                 eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
+        // Verify mNetworkCapabilities is updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
 
         // Mock the MOBIKE procedure
         vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
@@ -2535,15 +2578,11 @@
         // Expect 2 times: one for initial setup and one for MOBIKE
         verifyApplyTunnelModeTransforms(2);
 
-        // Verify mNetworkCapabilities and mNetworkAgent are updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+        // Verify mNetworkAgent is updated
         verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
         verify(mMockNetworkAgent, never()).unregister();
-
+        // No further doSetUnderlyingNetworks interaction. The interaction count should stay one.
+        verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any());
         vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
     }
 
@@ -2559,6 +2598,15 @@
 
         // Mock new network available & MOBIKE procedures
         vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
+                new NetworkCapabilities.Builder().build());
+        // Verify mNetworkCapabilities is updated
+        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
+                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+
         vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
         vpnSnapShot.childCb.onIpSecTransformsMigrated(
                 createIpSecTransform(), createIpSecTransform());
@@ -2817,15 +2865,34 @@
         // Verify MOBIKE is triggered
         verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
                 0 /* retryIndex */);
+        // Validation failure on VPN network should trigger a re-evaluation request for the
+        // underlying network.
+        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
 
         reset(mIkev2SessionCreator);
+        reset(mExecutor);
 
         // Send validation status update.
         // Recovered and get network validated. It should not trigger the ike session reset.
         ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
                 NetworkAgent.VALIDATION_STATUS_VALID);
+        // Verify that the retry count is reset. The mValidationFailRetryCount will not be reset
+        // until the executor finishes the execute() call, so wait until the all tasks are executed.
+        waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS);
+        assertEquals(0,
+                ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount);
         verify(mIkev2SessionCreator, never()).createIkeSession(
                 any(), any(), any(), any(), any(), any());
+
+        reset(mIkeSessionWrapper);
+        reset(mExecutor);
+
+        // Another validation fail should trigger another reportNetworkConnectivity
+        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
+                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
+        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
+                0 /* retryIndex */);
+        verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false);
     }
 
     @Test
@@ -2839,7 +2906,9 @@
                 NetworkAgent.VALIDATION_STATUS_NOT_VALID);
         verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
                 retry++);
-
+        // Validation failure on VPN network should trigger a re-evaluation request for the
+        // underlying network.
+        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
         reset(mIkev2SessionCreator);
 
         // Second validation status update.
@@ -2847,6 +2916,8 @@
                 NetworkAgent.VALIDATION_STATUS_NOT_VALID);
         verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
                 retry++);
+        // Call to reportNetworkConnectivity should only happen once. No further interaction.
+        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
 
         // Use real delay to verify reset session will not be performed if there is an existing
         // recovery for resetting the session.
@@ -2863,6 +2934,8 @@
                 eq(TimeUnit.MILLISECONDS));
         final List<Long> delays = delayCaptor.getAllValues();
         assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
+        // Call to reportNetworkConnectivity should only happen once. No further interaction.
+        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
 
         // Another invalid status reported should not trigger other scheduled recovery.
         expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
@@ -2874,6 +2947,8 @@
         // Verify that session being reset
         verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
                 .createIkeSession(any(), any(), any(), any(), any(), any());
+        // Call to reportNetworkConnectivity should only happen once. No further interaction.
+        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 9b38fea..8b10e0b 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -33,7 +33,10 @@
 import com.android.testutils.waitForIdle
 import java.net.NetworkInterface
 import java.util.Objects
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
 import org.junit.After
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -56,7 +59,9 @@
 private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 5
 private const val TIMEOUT_MS = 10_000L
 private val TEST_ADDR = parseNumericAddress("2001:db8::123")
+private val TEST_ADDR2 = parseNumericAddress("2001:db8::124")
 private val TEST_LINKADDR = LinkAddress(TEST_ADDR, 64 /* prefixLength */)
+private val TEST_LINKADDR2 = LinkAddress(TEST_ADDR2, 64 /* prefixLength */)
 private val TEST_NETWORK_1 = mock(Network::class.java)
 private val TEST_SOCKETKEY_1 = SocketKey(1001 /* interfaceIndex */)
 private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
@@ -64,6 +69,8 @@
 private const val TEST_SUBTYPE = "_subtype"
 private val TEST_INTERFACE1 = "test_iface1"
 private val TEST_INTERFACE2 = "test_iface2"
+private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
+private val TEST_OFFLOAD_PACKET2 = byteArrayOf(0x02, 0x03, 0x04)
 
 private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     port = 12345
@@ -102,7 +109,7 @@
     OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
     listOf(TEST_SUBTYPE),
     "Android_test.local",
-    null, /* rawOffloadPacket */
+    TEST_OFFLOAD_PACKET1,
     0, /* priority */
     OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
 )
@@ -111,7 +118,16 @@
     OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
     listOf(),
     "Android_test.local",
-    null, /* rawOffloadPacket */
+    TEST_OFFLOAD_PACKET1,
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
+private val OFFLOAD_SERVICEINFO_NO_SUBTYPE2 = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(),
+    "Android_test.local",
+    TEST_OFFLOAD_PACKET2,
     0, /* priority */
     OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
 )
@@ -147,6 +163,10 @@
         doReturn(createEmptyNetworkInterface()).`when`(mockSocket2).getInterface()
         doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
         doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+            SERVICE_ID_1)
+        doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
+            SERVICE_ID_1)
     }
 
     @After
@@ -189,10 +209,35 @@
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
+        // Service is conflicted.
+        postSync { intAdvCbCaptor.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+
+        // Verify the metrics data
+        doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(40).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(25, metrics.mRepliedRequestsCount)
+        assertEquals(40, metrics.mSentPacketCount)
+        assertEquals(0, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
+
+        doReturn(TEST_OFFLOAD_PACKET2).`when`(mockInterfaceAdvertiser1)
+            .getRawOffloadPayload(
+                SERVICE_ID_1
+            )
+        postSync {
+            socketCb.onAddressesChanged(
+                TEST_SOCKETKEY_1,
+                mockSocket1,
+                listOf(TEST_LINKADDR2)
+            )
+        }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
+
         postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         verify(mockInterfaceAdvertiser1).destroyNow()
         postSync { intAdvCbCaptor.value.onDestroyed(mockSocket1) }
-        verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
+        verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
     }
 
     @Test
@@ -235,6 +280,22 @@
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
                 argThat { it.matches(ALL_NETWORKS_SERVICE) })
 
+        // Services are conflicted.
+        postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        postSync { intAdvCbCaptor2.value.onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1) }
+
+        // Verify the metrics data
+        doReturn(10).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(5).`when`(mockInterfaceAdvertiser2).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(22).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        doReturn(12).`when`(mockInterfaceAdvertiser2).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(15, metrics.mRepliedRequestsCount)
+        assertEquals(34, metrics.mSentPacketCount)
+        assertEquals(2, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
+
         // Unregister the service
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockInterfaceAdvertiser1).removeService(SERVICE_ID_1)
@@ -346,6 +407,14 @@
         handler.post(r)
         handler.waitForIdle(TIMEOUT_MS)
     }
+
+    private fun <T> postReturn(r: (() -> T)): T {
+        val future = CompletableFuture<T>()
+        handler.post {
+            future.complete(r())
+        }
+        return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
 }
 
 // NsdServiceInfo does not implement equals; this is useful to use in argument matchers
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index 12faa50..c39ee1e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -253,7 +253,7 @@
 
         val captor = ArgumentCaptor.forClass(DatagramPacket::class.java)
         repeat(FIRST_ANNOUNCES_COUNT) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request, 1 /* sentPacketCount */)
             verify(socket, atLeast(i + 1)).send(any())
             val now = SystemClock.elapsedRealtime()
             assertTrue(now > timeStart + startDelay + i * FIRST_ANNOUNCES_DELAY)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 5ca4dd6..f284819 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -92,7 +92,7 @@
 
     private fun assertProbesSent(probeInfo: TestProbeInfo, expectedHex: String) {
         repeat(probeInfo.numSends) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo, 1 /* sentPacketCount */)
             // If the probe interval is short, more than (i+1) probes may have been sent already
             verify(socket, atLeast(i + 1)).send(any())
         }
@@ -190,7 +190,7 @@
         prober.startProbing(probeInfo)
 
         // Expect the initial probe
-        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo)
+        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo, 1 /* sentPacketCount */)
 
         // Stop probing
         val stopResult = CompletableFuture<Boolean>()
@@ -200,7 +200,7 @@
 
         // Wait for a bit (more than the probe delay) to ensure no more probes were sent
         Thread.sleep(SHORT_TIMEOUT_MS * 2)
-        verify(cb, never()).onSent(1, probeInfo)
+        verify(cb, never()).onSent(1, probeInfo, 1 /* sentPacketCount */)
         verify(cb, never()).onFinished(probeInfo)
 
         // Only one sent packet
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 0033b5a..af47b1c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -155,7 +155,7 @@
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
         repository.onProbingSucceeded(probingInfo)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         repository.exitService(TEST_SERVICE_ID_1)
@@ -166,7 +166,7 @@
     fun testExitAnnouncements() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
@@ -195,7 +195,7 @@
     fun testExitAnnouncements_WithSubtype() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
@@ -230,7 +230,7 @@
     fun testExitingServiceReAdded() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         repository.exitService(TEST_SERVICE_ID_1)
 
         assertEquals(TEST_SERVICE_ID_1,
@@ -246,7 +246,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
                 TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         val packet = announcementInfo.getPacket(0)
 
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
@@ -366,6 +366,58 @@
     }
 
     @Test
+    fun testGetOffloadPacket() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+        val serviceType = arrayOf("_testservice", "_tcp", "local")
+        val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1)
+        assertEquals(0x8400, offloadPacket.flags)
+        assertEquals(0, offloadPacket.questions.size)
+        assertEquals(0, offloadPacket.additionalRecords.size)
+        assertEquals(0, offloadPacket.authorityRecords.size)
+        assertContentEquals(listOf(
+            MdnsPointerRecord(
+                serviceType,
+                0L /* receiptTimeMillis */,
+                // Not a unique name owned by the announcer, so cacheFlush=false
+                false /* cacheFlush */,
+                4500000L /* ttlMillis */,
+                serviceName),
+            MdnsServiceRecord(
+                serviceName,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                TEST_PORT /* servicePort */,
+                TEST_HOSTNAME),
+            MdnsTextRecord(
+                serviceName,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                4500000L /* ttlMillis */,
+                emptyList() /* entries */),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[0].address),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[1].address),
+            MdnsInetAddressRecord(TEST_HOSTNAME,
+                0L /* receiptTimeMillis */,
+                true /* cacheFlush */,
+                120000L /* ttlMillis */,
+                TEST_ADDRESSES[2].address),
+        ), offloadPacket.answers)
+    }
+
+    @Test
     fun testGetReverseDnsAddress() {
         val expectedV6 = "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa"
                 .split(".").toTypedArray()
@@ -605,6 +657,34 @@
         // Above records are identical to the actual registrations: no conflict
         assertEquals(emptySet(), repository.getConflictingServices(packet))
     }
+
+    @Test
+    fun testGetServiceRepliedRequestsCount() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        // Verify that there is no packet replied.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        val questions = listOf(MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                // TTL and data is empty for a question
+                0L /* ttlMillis */,
+                null /* pointer */))
+        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+        // Reply to the question and verify there is one packet replied.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(1, repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        // No package replied for unknown service.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_2))
+    }
 }
 
 private fun MdnsRecordRepository.initWithService(