Merge "Delay frozen app sockets close until the cellular modem wakes up" 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/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index bb09d0d..eadba58 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -283,6 +283,7 @@
     private List<TetheredClient> mDhcpLeases = Collections.emptyList();
 
     private int mLastIPv6UpstreamIfindex = 0;
+    private boolean mUpstreamSupportsBpf = false;
 
     private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
         public void accept(NeighborEvent e) {
@@ -779,15 +780,15 @@
 
         // If v6only is null, we pass in null to setRaParams(), which handles
         // deprecation of any existing RA data.
-
         setRaParams(params);
-        // Be aware that updateIpv6ForwardingRules use mLastIPv6LinkProperties, so this line should
-        // be eariler than updateIpv6ForwardingRules.
-        // TODO: avoid this dependencies and move this logic into BpfCoordinator.
-        mLastIPv6LinkProperties = v6only;
 
-        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null);
+        // Not support BPF on virtual upstream interface
+        final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
+        updateIpv6ForwardingRules(
+                mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf, null);
+        mLastIPv6LinkProperties = v6only;
         mLastIPv6UpstreamIfindex = upstreamIfIndex;
+        mUpstreamSupportsBpf = upstreamSupportsBpf;
         if (mDadProxy != null) {
             mDadProxy.setUpstreamIface(upstreamIfaceParams);
         }
@@ -921,20 +922,14 @@
         mBpfCoordinator.tetherOffloadRuleUpdate(this, newIfindex);
     }
 
-    private boolean isIpv6VcnNetworkInterface() {
-        if (mLastIPv6LinkProperties == null) return false;
-
-        return isVcnInterface(mLastIPv6LinkProperties.getInterfaceName());
-    }
-
     // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream
     // changes or if a neighbor event is received.
     private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
-            NeighborEvent e) {
-        // If no longer have an upstream or it is virtual network, clear forwarding rules and do
+            boolean upstreamSupportsBpf, NeighborEvent e) {
+        // If no longer have an upstream or upstream not supports BPF, clear forwarding rules and do
         // nothing else.
         // TODO: Rather than always clear rules, ensure whether ipv6 ever enable first.
-        if (upstreamIfindex == 0 || isIpv6VcnNetworkInterface()) {
+        if (upstreamIfindex == 0 || !upstreamSupportsBpf) {
             clearIpv6ForwardingRules();
             return;
         }
@@ -995,7 +990,8 @@
         if (mInterfaceParams != null
                 && mInterfaceParams.index == e.ifindex
                 && mInterfaceParams.hasMacAddress) {
-            updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e);
+            updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex,
+                    mUpstreamSupportsBpf, e);
             updateClientInfoIpv4(e);
         }
     }
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/Android.bp b/framework/Android.bp
index e663764..e577e6d 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -80,6 +80,7 @@
     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",
     ],
@@ -127,6 +128,7 @@
         // 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/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/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 2ba59da..dfaf8cf 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -27,6 +27,7 @@
     srcs: [":remoteauth-service-srcs"],
     required: ["libremoteauth_jni_rust_defaults"],
     defaults: [
+        "enable-remoteauth-targets",
         "framework-system-server-module-defaults",
     ],
     libs: [
@@ -47,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+
diff --git a/remoteauth/service/java/com/android/server/remoteauth/README.md b/remoteauth/service/java/com/android/server/remoteauth/README.md
index b2b5aab..b659bf7 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/README.md
+++ b/remoteauth/service/java/com/android/server/remoteauth/README.md
@@ -6,3 +6,5 @@
 ## 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
index 2b5efff..36aa585 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingCapabilities.java
@@ -15,6 +15,9 @@
  */
 package com.android.server.remoteauth.ranging;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import androidx.annotation.IntDef;
 
 import com.google.common.collect.ImmutableList;
@@ -43,6 +46,8 @@
     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.
@@ -53,13 +58,28 @@
         return mSupportedRangingMethods;
     }
 
-    private RangingCapabilities(List<Integer> supportedRangingMethods) {
+    /**
+     * 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) {
@@ -67,9 +87,18 @@
             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);
+            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
index 989b5ed..cf0db76 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingManager.java
@@ -15,7 +15,21 @@
  */
 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
@@ -25,16 +39,46 @@
  * outside of this class.
  */
 public class RangingManager {
+    private static final String TAG = "RangingManager";
 
-    public RangingManager(Context context) {}
+    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() {
-        return null;
+        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;
     }
 
     /**
@@ -42,9 +86,35 @@
      * provided based on the rangingCapabilities of the device.
      *
      * @param sessionParameters parameters used to setup the session.
-     * @return the created RangingSession.
+     * @return the created RangingSession. Null if session creation failed.
+     * @throws IllegalArgumentException if sessionParameters is invalid.
      */
-    public RangingSession createSession(SessionParameters sessionParameters) {
+    @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/RangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
index 9ef6bda..adb36c5 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
@@ -16,9 +16,16 @@
 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;
@@ -35,18 +42,50 @@
  * <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 {
         /**
@@ -55,7 +94,8 @@
          * @param sessionInfo info about this ranging session.
          * @param rangingReport new ranging report
          */
-        void onRangingReport(SessionInfo sessionInfo, RangingReport rangingReport);
+        void onRangingReport(
+                @NonNull SessionInfo sessionInfo, @NonNull RangingReport rangingReport);
 
         /**
          * Call upon any ranging error events.
@@ -63,7 +103,49 @@
          * @param sessionInfo info about this ranging session.
          * @param rangingError error type
          */
-        void onError(SessionInfo sessionInfo, @RangingError int rangingError);
+        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());
+        }
     }
 
     /**
@@ -75,6 +157,8 @@
      * @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,
@@ -94,8 +178,22 @@
      * 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(byte[] baseKey) {}
+    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.
@@ -105,6 +203,52 @@
      * 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(byte[] syncData) {}
+    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
index 5e4fc48..0ec640c 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionInfo.java
@@ -41,6 +41,16 @@
         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 = "";
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
index 33c3203..2f71244 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/SessionParameters.java
@@ -19,9 +19,14 @@
 
 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.
  *
@@ -31,9 +36,29 @@
  */
 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;
@@ -51,6 +76,11 @@
         return mRangingMethod;
     }
 
+    @DeviceRole
+    public int getDeviceRole() {
+        return mDeviceRole;
+    }
+
     public float getLowerProximityBoundaryM() {
         return mLowerProximityBoundaryM;
     }
@@ -74,6 +104,7 @@
     private SessionParameters(
             String deviceId,
             @RangingMethod int rangingMethod,
+            @DeviceRole int deviceRole,
             float lowerProximityBoundaryM,
             float upperProximityBoundaryM,
             boolean autoDeriveParams,
@@ -81,6 +112,7 @@
             byte[] syncData) {
         mDeviceId = deviceId;
         mRangingMethod = rangingMethod;
+        mDeviceRole = deviceRole;
         mLowerProximityBoundaryM = lowerProximityBoundaryM;
         mUpperProximityBoundaryM = upperProximityBoundaryM;
         mAutoDeriveParams = autoDeriveParams;
@@ -92,6 +124,7 @@
     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;
@@ -120,6 +153,12 @@
             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.
          *
@@ -191,6 +230,7 @@
             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);
@@ -212,6 +252,7 @@
             return new SessionParameters(
                     mDeviceId,
                     mRangingMethod,
+                    mDeviceRole,
                     mLowerProximityBoundaryM,
                     mUpperProximityBoundaryM,
                     mAutoDeriveParams,
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/src/lib.rs b/remoteauth/service/jni/src/lib.rs
index 0c18679..a816c94 100644
--- a/remoteauth/service/jni/src/lib.rs
+++ b/remoteauth/service/jni/src/lib.rs
@@ -19,6 +19,7 @@
 
 mod jnames;
 mod unique_jvm;
+mod utils;
 
-//pub mod remoteauth_jni_android_protocol;
 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
index 4597561..1967c1a 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -15,7 +15,6 @@
 use crate::jnames::{SEND_REQUEST_MNAME, SEND_REQUEST_MSIG};
 use crate::unique_jvm;
 use anyhow::anyhow;
-use async_trait::async_trait;
 use jni::errors::Error as JNIError;
 use jni::objects::{GlobalRef, JMethodID, JObject, JValue};
 use jni::signature::TypeSignature;
@@ -26,11 +25,7 @@
 use std::collections::HashMap;
 use std::sync::{
     atomic::{AtomicI64, Ordering},
-    Arc,
-};
-use tokio::{
-    runtime::Runtime,
-    sync::{mpsc, Mutex},
+    Arc, Mutex,
 };
 
 /// Macro capturing the name of the function calling this macro.
@@ -65,7 +60,7 @@
     HANDLE_RN.fetch_add(1, Ordering::SeqCst)
 }
 
-async fn insert_platform_handle(handle: i64, item: Arc<Mutex<JavaPlatform>>) {
+fn insert_platform_handle(handle: i64, item: Arc<Mutex<JavaPlatform>>) {
     if 0 == handle {
         // Init once
         logger::init(
@@ -75,14 +70,22 @@
                 .with_filter("trace,jni=info"),
         );
     }
-    HANDLE_MAPPING.lock().await.insert(handle, Arc::clone(&item));
+    HANDLE_MAPPING.lock().unwrap().insert(handle, Arc::clone(&item));
 }
 
-#[async_trait]
+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.
-    async fn send_request(&mut self, connection_id: i32, request: &[u8])
-        -> anyhow::Result<Vec<u8>>;
+    fn send_request(
+        &mut self,
+        connection_id: i32,
+        request: &[u8],
+        callback: Box<dyn ResponseCallback + Send>,
+    ) -> anyhow::Result<()>;
 }
 //////////////////////////////////
 
@@ -91,25 +94,22 @@
     vm: &'static Arc<JavaVM>,
     platform_native_obj: GlobalRef,
     send_request_method_id: JMethodID,
-    map_futures: Mutex<HashMap<i64, mpsc::Sender<Vec<u8>>>>,
+    map_futures: Mutex<HashMap<i64, Box<dyn ResponseCallback + Send>>>,
     atomic_handle: AtomicI64,
 }
 
 impl JavaPlatform {
     // Method to create JavaPlatform
-    pub async fn create<'a>(
-        env: JNIEnv<'a>,
-        java_platform_native: JObject<'a>,
+    pub fn create(
+        java_platform_native: JObject<'_>,
     ) -> Result<Arc<Mutex<impl Platform>>, JNIError> {
-        let jvm = env.get_java_vm()?;
-        let _ = unique_jvm::set_once(jvm);
         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)).await;
+        insert_platform_handle(platform_handle, Arc::clone(&platform));
         Ok(Arc::clone(&platform))
     }
 
@@ -136,19 +136,18 @@
     }
 }
 
-#[async_trait]
 impl Platform for JavaPlatform {
-    async fn send_request(
+    fn send_request(
         &mut self,
         connection_id: i32,
         request: &[u8],
-    ) -> anyhow::Result<Vec<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 (tx, mut rx) = mpsc::channel(1);
         let response_handle = self.atomic_handle.fetch_add(1, Ordering::SeqCst);
-        self.map_futures.lock().await.insert(response_handle, tx);
+        self.map_futures.lock().unwrap().insert(response_handle, callback);
         self.vm
             .attach_current_thread()
             .and_then(|env| {
@@ -175,21 +174,20 @@
                 ))
             })
             .map_err(|e| anyhow!("JNI: Failed to attach current thread: {:?}", e))?;
-
-        rx.recv().await.ok_or(anyhow!("{} failed in awaiting for a result", function_name!()))
+        Ok(())
     }
 }
 
 impl JavaPlatform {
-    async fn on_send_request_success(&mut self, response: &[u8], response_handle: i64) {
+    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(tx) = self.map_futures.lock().await.remove(&response_handle) {
-            let _ = tx.send(response.to_vec()).await;
+        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 {}:{}",
@@ -200,7 +198,7 @@
         }
     }
 
-    async fn on_send_request_error(&self, error_code: i32, response_handle: i64) {
+    fn on_send_request_error(&self, error_code: i32, response_handle: i64) {
         error!(
             "{} completed with error {} {}:{}",
             function_name!(),
@@ -208,12 +206,11 @@
             self.platform_handle,
             response_handle
         );
-        if let Some(tx) = self.map_futures.lock().await.remove(&response_handle) {
-            // `rx.recv()` ends with `Err`
-            drop(tx);
+        if let Some(mut callback) = self.map_futures.lock().unwrap().remove(&response_handle) {
+            callback.on_error(error_code);
         } else {
             error!(
-                "Failed to find TX for {} and {}:{}",
+                "Failed to find callback for {} and {}:{}",
                 function_name!(),
                 self.platform_handle,
                 response_handle
@@ -231,25 +228,20 @@
     response_handle: jlong,
 ) {
     debug!("{}: enter", function_name!());
-    Runtime::new().unwrap().block_on(native_on_send_request_success(
-        env,
-        app_response,
-        platform_handle,
-        response_handle,
-    ));
+    native_on_send_request_success(env, app_response, platform_handle, response_handle);
 }
 
-async fn native_on_send_request_success(
+fn native_on_send_request_success(
     env: JNIEnv<'_>,
     app_response: jbyteArray,
     platform_handle: jlong,
     response_handle: jlong,
 ) {
-    if let Some(platform) = HANDLE_MAPPING.lock().await.get(&platform_handle) {
+    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().await;
-        platform.on_send_request_success(&response, response_handle).await;
+        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",
@@ -267,23 +259,18 @@
     response_handle: jlong,
 ) {
     debug!("{}: enter", function_name!());
-    Runtime::new().unwrap().block_on(native_on_send_request_error(
-        env,
-        error_code,
-        platform_handle,
-        response_handle,
-    ));
+    native_on_send_request_error(env, error_code, platform_handle, response_handle);
 }
 
-async fn native_on_send_request_error(
+fn native_on_send_request_error(
     env: JNIEnv<'_>,
     error_code: jint,
     platform_handle: jlong,
     response_handle: jlong,
 ) {
-    if let Some(platform) = HANDLE_MAPPING.lock().await.get(&platform_handle) {
-        let platform = (*platform).lock().await;
-        platform.on_send_request_error(error_code, response_handle).await;
+    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",
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/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
index e6b6e3b..8135b4f 100644
--- a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingCapabilitiesTest.java
@@ -28,16 +28,25 @@
 /** 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/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/SessionParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
index 357fdf9..522623e 100644
--- a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/SessionParametersTest.java
@@ -17,6 +17,7 @@
 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;
@@ -25,6 +26,7 @@
 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;
@@ -35,6 +37,7 @@
 
     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;
@@ -55,6 +58,7 @@
                 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)
@@ -82,6 +86,7 @@
         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)
@@ -95,6 +100,21 @@
         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)
@@ -109,6 +129,7 @@
                 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)
@@ -123,6 +144,7 @@
                 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)
@@ -138,6 +160,7 @@
                 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)
@@ -154,6 +177,7 @@
                 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)
@@ -168,6 +192,7 @@
                 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)
@@ -183,6 +208,7 @@
                 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)
@@ -197,6 +223,7 @@
                 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)
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/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..27e97f1 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -17,15 +17,18 @@
 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.MdnsRecord.MAX_LABEL_LENGTH;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 
@@ -75,6 +78,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;
@@ -1191,7 +1195,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 +1881,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 +1919,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 +2102,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 +2113,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) {
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..f43df45 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -37,6 +37,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 +118,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
@@ -385,9 +397,12 @@
         }
     }
 
-    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;
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/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/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 49a6ef1..508e924 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,10 +891,11 @@
         }
     }
 
-    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)
@@ -907,36 +909,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>()
         }
     }
 
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index f0c7dcc..695cfe8 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);
@@ -1664,6 +1676,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);
     }