Merge "ethernet: rename IpClient callback handler functions" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index cf9b359..d3b01ea 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -203,6 +203,8 @@
         // result in a build failure due to inconsistent flags.
         package_prefixes: [
             "android.nearby.aidl",
+            "android.remoteauth.aidl",
+            "android.remoteauth",
             "android.net.apf",
             "android.net.connectivity",
             "android.net.http.apihelpers",
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 5e08aba..20f0bc6 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -25,11 +25,12 @@
     ],
     min_sdk_version: "30",
     static_libs: [
-        "NetworkStackApiStableLib",
+        "DhcpPacketLib",
         "androidx.test.rules",
         "cts-net-utils",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
+        "net-utils-device-common",
         "net-utils-device-common-bpf",
         "testables",
         "connectivity-net-module-utils-bpf",
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index 3f3768e..4f3c6e7 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -74,6 +74,7 @@
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.arp.ArpPacket;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -85,7 +86,6 @@
 import com.android.net.module.util.structs.RaHeader;
 import com.android.net.module.util.structs.TcpHeader;
 import com.android.net.module.util.structs.UdpHeader;
-import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 05b84c2..dacdaf2 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -43,6 +43,7 @@
         ":framework-connectivity-tiramisu-updatable-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
+        ":framework-remoteauth-java-sources",
     ],
     libs: [
         "unsupportedappusage",
@@ -130,8 +131,10 @@
         "android.net",
         "android.net.nsd",
         "android.nearby",
+        "android.remoteauth",
         "com.android.connectivity",
         "com.android.nearby",
+        "com.android.remoteauth",
     ],
 
     hidden_api: {
@@ -153,6 +156,7 @@
         "//packages/modules/Connectivity/service", // For R8 only
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/nearby:__subpackages__",
+        "//packages/modules/Connectivity/remoteauth:__subpackages__",
         "//frameworks/base",
 
         // Tests using hidden APIs
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 5a8d47b..42c83d8 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -207,3 +207,43 @@
 
 }
 
+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 8de8097..813e296 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -62,7 +62,6 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
-        ":framework-remoteauth-java-sources",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -153,7 +152,6 @@
         "//packages/modules/Connectivity/framework-t",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service-t",
-        "//packages/modules/Connectivity/remoteauth:__subpackages__",
         "//frameworks/base/packages/Connectivity/service",
         "//frameworks/base",
 
diff --git a/remoteauth/TEST_MAPPING b/remoteauth/TEST_MAPPING
index 5ad8da6..5061319 100644
--- a/remoteauth/TEST_MAPPING
+++ b/remoteauth/TEST_MAPPING
@@ -7,7 +7,7 @@
   // TODO(b/193602229): uncomment once it's supported.
   //"mainline-presubmit": [
   //  {
-  //    "name": "RemoteAuthUnitTests[com.google.android.tethering.apex]"
+  //    "name": "RemoteAuthUnitTests[com.google.android.remoteauth.apex]"
   //  }
   //]
 }
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 48d10b6..71b621a 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -25,7 +25,7 @@
     ],
     path: "java",
     visibility: [
-        "//packages/modules/Connectivity/framework:__subpackages__",
+        "//packages/modules/Connectivity/framework-t:__subpackages__",
     ],
 }
 
@@ -43,7 +43,13 @@
     name: "framework-remoteauth-static",
     srcs: [":framework-remoteauth-java-sources"],
     sdk_version: "module_current",
-    libs: [],
-    static_libs: [],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-bluetooth",
+    ],
+    static_libs: [
+        "modules-utils-preconditions",
+    ],
     visibility: ["//packages/modules/Connectivity/remoteauth/tests:__subpackages__"],
 }
diff --git a/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
new file mode 100644
index 0000000..f53e2dc
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.remoteauth;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Reports newly discovered remote devices.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public interface DeviceDiscoveryCallback {
+    /** The device is no longer seen in the discovery process. */
+    int STATE_LOST = 0;
+    /** The device is seen in the discovery process */
+    int STATE_SEEN = 1;
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_LOST, STATE_SEEN})
+    @interface State {}
+
+    /**
+     * Invoked for every change in remote device state.
+     *
+     * @param device remote device
+     * @param state indicates if found or lost
+     */
+    void onDeviceUpdate(@NonNull RemoteDevice device, @State int state);
+
+    /** Invoked when discovery is stopped due to timeout. */
+    void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
new file mode 100644
index 0000000..2ad6a6a
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.remoteauth;
+
+import android.remoteauth.RemoteDevice;
+
+/**
+ * Binder callback for DeviceDiscoveryCallback.
+ *
+ * {@hide}
+ */
+oneway interface IDeviceDiscoveryListener {
+        /** Reports a {@link RemoteDevice} being discovered. */
+        void onDiscovered(in RemoteDevice remoteDevice);
+
+        /** Reports a {@link RemoteDevice} is no longer within range. */
+        void onLost(in RemoteDevice remoteDevice);
+
+        /** Reports a timeout of {@link RemoteDevice} was reached. */
+        void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
new file mode 100644
index 0000000..f4387e3
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.remoteauth;
+
+import android.remoteauth.IDeviceDiscoveryListener;
+
+/**
+ * Interface for communicating with the RemoteAuthService.
+ * These methods are all require MANAGE_REMOTE_AUTH signature permission.
+ * @hide
+ */
+interface IRemoteAuthService {
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean isRemoteAuthSupported();
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean registerDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                  int userId,
+                                  int timeoutMs,
+                                  String packageName,
+                                  @nullable String attributionTag);
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    void unregisterDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                     int userId,
+                                     String packageName,
+                                     @nullable String attributionTag);
+}
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
new file mode 100644
index 0000000..dfd7726
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.remoteauth;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for initializing RemoteAuth service.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class RemoteAuthFrameworkInitializer {
+    private RemoteAuthFrameworkInitializer() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers all Nearby
+     * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called from anywhere besides {@link
+     *     SystemServiceRegistry}
+     */
+    public static void registerServiceWrappers() {
+        // TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
+        // is automerges from aosp-main to udc-mainline-prod
+        SystemServiceRegistry.registerContextAwareService(
+                RemoteAuthManager.REMOTE_AUTH_SERVICE,
+                RemoteAuthManager.class,
+                (context, serviceBinder) -> {
+                    IRemoteAuthService service = IRemoteAuthService.Stub.asInterface(serviceBinder);
+                    return new RemoteAuthManager(context, service);
+                });
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
new file mode 100644
index 0000000..c025a55
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.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;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * A system service providing a way to perform remote authentication-related operations such as
+ * discovering, registering and authenticating via remote authenticator.
+ *
+ * <p>To get a {@link RemoteAuthManager} instance, call the <code>
+ * Context.getSystemService(Context.REMOTE_AUTH_SERVICE)</code>.
+ *
+ * @hide
+ */
+@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)
+public class RemoteAuthManager {
+    private static final String TAG = "RemoteAuthManager";
+
+    /** @hide */
+    public static final String REMOTE_AUTH_SERVICE = "remote_auth";
+
+    private final Context mContext;
+    private final IRemoteAuthService mService;
+
+    @GuardedBy("mDiscoveryListeners")
+    private final WeakHashMap<
+                    DeviceDiscoveryCallback, WeakReference<DeviceDiscoveryListenerTransport>>
+            mDiscoveryListeners = new WeakHashMap<>();
+
+    /** @hide */
+    public RemoteAuthManager(@NonNull Context context, @NonNull IRemoteAuthService service) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(service);
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Returns if this device can be enrolled in the feature.
+     *
+     * @return true if this device can be enrolled
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean isRemoteAuthSupported() {
+        try {
+            return mService.isRemoteAuthSupported();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Starts remote authenticator discovery process with timeout. Devices that are capable to
+     * operate as remote authenticators are reported via callback. The discovery stops by calling
+     * stopDiscovery or after a timeout.
+     *
+     * @param timeoutMs the duration in milliseconds after which discovery will stop automatically
+     * @param executor the callback will be executed in the executor thread
+     * @param callback to be used by the caller to get notifications about remote devices
+     * @return {@code true} if discovery began successfully, {@code false} otherwise
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean startDiscovery(
+            int timeoutMs,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull DeviceDiscoveryCallback callback) {
+        try {
+            Preconditions.checkNotNull(callback, "invalid null callback");
+            Preconditions.checkArgument(timeoutMs > 0, "invalid timeoutMs, must be > 0");
+            Preconditions.checkNotNull(executor, "invalid null executor");
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.get(callback);
+                transport = (reference != null) ? reference.get() : null;
+                if (transport == null) {
+                    transport =
+                            new DeviceDiscoveryListenerTransport(
+                                    callback, mContext.getUser().getIdentifier(), executor);
+                }
+
+                boolean result =
+                        mService.registerDiscoveryListener(
+                                transport,
+                                mContext.getUser().getIdentifier(),
+                                timeoutMs,
+                                mContext.getPackageName(),
+                                mContext.getAttributionTag());
+                if (result) {
+                    mDiscoveryListeners.put(callback, new WeakReference<>(transport));
+                    return true;
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+    /**
+     * Removes this listener from device discovery notifications. The given callback is guaranteed
+     * not to receive any invocations that happen after this method is invoked.
+     *
+     * @param callback the callback for the previously started discovery to be ended
+     * @hide
+     */
+    // 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/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public void stopDiscovery(@NonNull DeviceDiscoveryCallback callback) {
+        Preconditions.checkNotNull(callback, "invalid null scanCallback");
+        try {
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.remove(callback);
+                transport = (reference != null) ? reference.get() : null;
+            }
+            if (transport != null) {
+                mService.unregisterDiscoveryListener(
+                        transport,
+                        transport.getUserId(),
+                        mContext.getPackageName(),
+                        mContext.getAttributionTag());
+            } else {
+                Log.d(
+                        TAG,
+                        "Cannot stop discovery with this callback "
+                                + "because it is not registered.");
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private class DeviceDiscoveryListenerTransport extends IDeviceDiscoveryListener.Stub {
+
+        private volatile @NonNull DeviceDiscoveryCallback mDeviceDiscoveryCallback;
+        private Executor mExecutor;
+        private @UserIdInt int mUserId;
+
+        DeviceDiscoveryListenerTransport(
+                DeviceDiscoveryCallback deviceDiscoveryCallback,
+                @UserIdInt int userId,
+                @CallbackExecutor Executor executor) {
+            Preconditions.checkNotNull(deviceDiscoveryCallback, "invalid null callback");
+            mDeviceDiscoveryCallback = deviceDiscoveryCallback;
+            mUserId = userId;
+            mExecutor = executor;
+        }
+
+        @UserIdInt
+        int getUserId() {
+            return mUserId;
+        }
+
+        @Override
+        public void onDiscovered(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onDiscovered is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about discovered: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_SEEN);
+                    });
+        }
+
+        @Override
+        public void onLost(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onLost is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about lost: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_LOST);
+                    });
+        }
+
+        @Override
+        public void onTimeout() {
+            Log.i(TAG, "Notifying the caller about discovery timeout");
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onTimeout();
+                    });
+            synchronized (mDiscoveryListeners) {
+                mDiscoveryListeners.remove(mDeviceDiscoveryCallback);
+            }
+            mDeviceDiscoveryCallback = null;
+        }
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
new file mode 100644
index 0000000..ea38be2
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.remoteauth;
+
+parcelable RemoteDevice;
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.java b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
new file mode 100644
index 0000000..4cd2399
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.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;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Remote device that can be registered as remote authenticator.
+ *
+ * @hide
+ */
+// TODO(b/295407748) Change to use @DataClass
+@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;
+    /** The remote device is registered as remote authenticator. */
+    public static final int STATE_REGISTERED = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_NOT_REGISTERED, STATE_REGISTERED})
+    @interface RegistrationState {}
+
+    @NonNull private final String mName;
+    private final @RegistrationState int mRegistrationState;
+    private final int mConnectionId;
+
+    public static final @NonNull Creator<RemoteDevice> CREATOR =
+            new Creator<>() {
+                @Override
+                public RemoteDevice createFromParcel(Parcel in) {
+                    RemoteDevice.Builder builder = new RemoteDevice.Builder();
+                    builder.setName(in.readString());
+                    builder.setRegistrationState(in.readInt());
+                    builder.setConnectionId(in.readInt());
+
+                    return builder.build();
+                }
+
+                @Override
+                public RemoteDevice[] newArray(int size) {
+                    return new RemoteDevice[size];
+                }
+            };
+
+    private RemoteDevice(
+            @Nullable String name,
+            @RegistrationState int registrationState,
+            @NonNull int connectionId) {
+        this.mName = name;
+        this.mRegistrationState = registrationState;
+        this.mConnectionId = connectionId;
+    }
+
+    /** Gets the name of the {@link RemoteDevice} device. */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /** Returns registration state of the {@link RemoteDevice}. */
+    public @RegistrationState int getRegistrationState() {
+        return mRegistrationState;
+    }
+
+    /** Returns connection id of the {@link RemoteDevice}. */
+    @NonNull
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Returns a string representation of {@link RemoteDevice}. */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RemoteDevice [");
+        sb.append("name=").append(mName).append(", ");
+        sb.append("registered=").append(mRegistrationState).append(", ");
+        sb.append("connectionId=").append(mConnectionId);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /** Returns true if this {@link RemoteDevice} object is equals to other. */
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof RemoteDevice) {
+            RemoteDevice otherDevice = (RemoteDevice) other;
+            return Objects.equals(this.mName, otherDevice.mName)
+                    && this.getRegistrationState() == otherDevice.getRegistrationState()
+                    && this.mConnectionId == otherDevice.mConnectionId;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mRegistrationState, mConnectionId);
+    }
+
+    /**
+     * Helper function for writing {@link RemoteDevice} to a Parcel.
+     *
+     * @param dest The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        String name = getName();
+        dest.writeString(name);
+        dest.writeInt(getRegistrationState());
+        dest.writeInt(getConnectionId());
+    }
+
+    /** Builder for {@link RemoteDevice} objects. */
+    public static final class Builder {
+        @Nullable private String mName;
+        // represents if device is already registered
+        private @RegistrationState int mRegistrationState;
+        private int mConnectionId;
+
+        private Builder() {
+        }
+
+        public Builder(final int connectionId) {
+            this.mConnectionId = connectionId;
+        }
+
+        /**
+         * Sets the name of the {@link RemoteDevice} device.
+         *
+         * @param name of the {@link RemoteDevice}. Can be {@code null} if there is no name.
+         */
+        @NonNull
+        public RemoteDevice.Builder setName(@Nullable String name) {
+            this.mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the registration state of the {@link RemoteDevice} device.
+         *
+         * @param registrationState of the {@link RemoteDevice}.
+         */
+        @NonNull
+        public RemoteDevice.Builder setRegistrationState(@RegistrationState int registrationState) {
+            this.mRegistrationState = registrationState;
+            return this;
+        }
+
+        /**
+         * Sets the connectionInfo of the {@link RemoteDevice} device.
+         *
+         * @param connectionId of the RemoteDevice.
+         */
+        @NonNull
+        public RemoteDevice.Builder setConnectionId(int connectionId) {
+            this.mConnectionId = connectionId;
+            return this;
+        }
+
+        /**
+         * Creates the {@link RemoteDevice} instance.
+         *
+         * @return the configured {@link RemoteDevice} instance.
+         */
+        @NonNull
+        public RemoteDevice build() {
+            return new RemoteDevice(mName, mRegistrationState, mConnectionId);
+        }
+    }
+}
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 5c5a2fb..c3a9fb3 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -29,8 +29,24 @@
     defaults: [
         "framework-system-server-module-defaults"
     ],
-    libs: [],
-    static_libs: [],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-bluetooth",
+        "error_prone_annotations",
+        "framework-configinfrastructure",
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-statsd",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+        "fast-pair-lite-protos",
+        "modules-utils-build",
+        "modules-utils-handlerexecutor",
+        "modules-utils-preconditions",
+        "modules-utils-backgroundthread",
+        "presence-lite-protos",
+    ],
     sdk_version: "system_server_current",
     // This is included in service-connectivity which is 30+
     // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
new file mode 100644
index 0000000..41ce89a
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 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;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.remoteauth.IDeviceDiscoveryListener;
+import android.remoteauth.IRemoteAuthService;
+
+import com.android.internal.util.Preconditions;
+
+/** Service implementing remoteauth functionality. */
+public class RemoteAuthService extends IRemoteAuthService.Stub {
+    public static final String TAG = "RemoteAuthService";
+
+    public RemoteAuthService(Context context) {
+        Preconditions.checkNotNull(context);
+        // TODO(b/290280702): Create here RemoteConnectivityManager and RangingManager
+    }
+
+    @Override
+    public boolean isRemoteAuthSupported() {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290676192): integrate with RangingManager
+        //  (check if UWB is supported by this device)
+        return true;
+    }
+
+    @Override
+    public boolean registerDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            int timeoutMs,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290280702): implement register discovery logic
+        return true;
+    }
+
+    @Override
+    public void unregisterDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290094221): implement unregister logic
+    }
+
+    private static void checkPermission(Context context, String permission) {
+        context.enforceCallingOrSelfPermission(permission,
+                "Must have " + permission + " permission.");
+    }
+}
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 8c08a1b..4b92d84 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -37,6 +37,7 @@
         "androidx.test.rules",
         "framework-remoteauth-static",
         "junit",
+        "libprotobuf-java-lite",
         "platform-test-annotations",
         "service-remoteauth-pre-jarjar",
         "truth-prebuilt",
diff --git a/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
index 5cf3e6b..6b43355 100644
--- a/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
+++ b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
@@ -23,6 +23,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -31,6 +32,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RemoteAuthManagerTest {
+    @Before
+    public void setUp() {}
+
     @Test
     public void testStub() {
         assertTrue(true);
diff --git a/service-t/Android.bp b/service-t/Android.bp
index ce5bb92..83caf35 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -56,6 +56,7 @@
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
         "service-thread-pre-jarjar",
+        "service-remoteauth-pre-jarjar",
         "ServiceConnectivityResources",
         "unsupportedappusage",
     ],
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 626c2eb..2da067a 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import android.content.Context;
+import android.remoteauth.RemoteAuthManager;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -25,6 +26,7 @@
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
+import com.android.server.remoteauth.RemoteAuthService;
 
 /**
  * Connectivity service initializer for core networking. This is called by system server to create
@@ -38,6 +40,7 @@
     private final NsdService mNsdService;
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
+    private final RemoteAuthService mRemoteAuthService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -49,6 +52,7 @@
         mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
+        mRemoteAuthService = createRemoteAuthService(context);
     }
 
     @Override
@@ -85,6 +89,11 @@
                     /* allowIsolated= */ false);
         }
 
+        if (mRemoteAuthService != null) {
+            Log.i(TAG, "Registering " + RemoteAuthManager.REMOTE_AUTH_SERVICE);
+            publishBinderService(RemoteAuthManager.REMOTE_AUTH_SERVICE, mRemoteAuthService,
+                    /* allowIsolated= */ false);
+        }
     }
 
     @Override
@@ -140,6 +149,20 @@
         }
     }
 
+    /** Return RemoteAuth service instance */
+    private RemoteAuthService createRemoteAuthService(final Context context) {
+        if (!SdkLevel.isAtLeastV()) return null;
+        try {
+            return new RemoteAuthService(context);
+        } 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);
+            return null;
+        }
+    }
+
     /**
      * Return EthernetServiceImpl instance or null if current SDK is lower than T or Ethernet
      * service isn't necessary.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index ab5024c..04d0b93 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -318,7 +318,6 @@
 import java.io.InterruptedIOException;
 import java.io.PrintWriter;
 import java.io.Writer;
-import java.lang.IllegalArgumentException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -11108,16 +11107,20 @@
 
         @Override
         public void onInterfaceLinkStateChanged(@NonNull String iface, boolean up) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceLinkStateChanged(iface, up);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.interfaceLinkStateChanged(iface, up);
+                }
+            });
         }
 
         @Override
         public void onInterfaceRemoved(@NonNull String iface) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceRemoved(iface);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.interfaceRemoved(iface);
+                }
+            });
         }
     }
 
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
index 92ffc5c..403d6b5 100644
--- a/tests/common/java/android/net/KeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.runner.AndroidJUnit4
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
 import java.net.InetAddress
 import java.util.Arrays
 import org.junit.Assert.assertEquals
@@ -50,34 +51,47 @@
 
     // Add for test because constructor of KeepalivePacketData is protected.
     private inner class TestKeepalivePacketData(
-        srcAddress: InetAddress = TEST_SRC_ADDRV4,
+        srcAddress: InetAddress? = TEST_SRC_ADDRV4,
         srcPort: Int = TEST_SRC_PORT,
-        dstAddress: InetAddress = TEST_DST_ADDRV4,
+        dstAddress: InetAddress? = TEST_DST_ADDRV4,
         dstPort: Int = TEST_DST_PORT,
         data: ByteArray = TESTBYTES
-    ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data)
+    ) : KeepalivePacketData(NonNullTestUtils.nullUnsafe(srcAddress), srcPort,
+            NonNullTestUtils.nullUnsafe(dstAddress), dstPort, data)
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testConstructor() {
-        var data: TestKeepalivePacketData
+        try {
+            TestKeepalivePacketData(srcAddress = null)
+            fail("Null src address should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
 
         try {
-            data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
+            TestKeepalivePacketData(dstAddress = null)
+            fail("Null dst address should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
             fail("Ip family mismatched should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(srcPort = INVALID_PORT)
+            TestKeepalivePacketData(srcPort = INVALID_PORT)
             fail("Invalid srcPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
         }
 
         try {
-            data = TestKeepalivePacketData(dstPort = INVALID_PORT)
+            TestKeepalivePacketData(dstPort = INVALID_PORT)
             fail("Invalid dstPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
index d8ec761..499d97f 100644
--- a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
@@ -16,12 +16,12 @@
 
 package android.net.cts
 
-import android.os.Build
 import android.content.Context
 import android.net.ConnectivityManager
 import android.net.NetworkInfo
 import android.net.NetworkInfo.DetailedState
 import android.net.NetworkInfo.State
+import android.os.Build
 import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -29,16 +29,17 @@
 import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
+import kotlin.reflect.jvm.isAccessible
+import kotlin.test.assertFails
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Rule
-import org.junit.runner.RunWith
 import org.junit.Test
-import kotlin.reflect.jvm.isAccessible
-import kotlin.test.assertFails
-import kotlin.test.assertFailsWith
+import org.junit.runner.RunWith
 
 const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE
 const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI
@@ -104,6 +105,15 @@
             NetworkInfo(ConnectivityManager.MAX_NETWORK_TYPE + 1,
                     TelephonyManager.NETWORK_TYPE_LTE, MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
         }
+
+        if (SdkLevel.isAtLeastT()) {
+            assertFailsWith<NullPointerException> {
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            }
+        } else {
+            // Doesn't immediately crash on S-
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+        }
     }
 
     @Test
@@ -126,11 +136,23 @@
         constructor.isAccessible = true
         val incorrectDetailedState = constructor.newInstance("any", 200) as DetailedState
         if (SdkLevel.isAtLeastT()) {
+            assertFailsWith<NullPointerException> {
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            }
+            assertFailsWith<NullPointerException> {
+                networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                        "reason", "extraInfo")
+            }
             // This actually throws ArrayOutOfBoundsException because of the implementation of
             // EnumMap, but that's an implementation detail so accept any crash.
             assertFails {
                 networkInfo.setDetailedState(incorrectDetailedState, "reason", "extraInfo")
             }
+        } else {
+            // Doesn't immediately crash on S-
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                    "reason", "extraInfo")
         }
     }
 }
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index f0da89f..a8414ca 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -49,6 +49,7 @@
 import android.telephony.TelephonyManager
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NonNullTestUtils
 import com.android.testutils.assertParcelSane
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -218,6 +219,19 @@
         templateNullWifiKey.assertDoesNotMatch(identWifiNullKey)
     }
 
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun testBuildTemplateMobileAll_nullSubscriberId() {
+        val templateMobileAllWithNullImsi =
+                buildTemplateMobileAll(NonNullTestUtils.nullUnsafe<String>(null))
+        val setWithNull = HashSet<String?>().apply {
+            add(null)
+        }
+        val templateFromBuilder = NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES)
+                .setSubscriberIds(setWithNull).build()
+        assertEquals(templateFromBuilder, templateMobileAllWithNullImsi)
+    }
+
     @Test
     fun testMobileMatches() {
         val templateMobileImsi1 = buildTemplateMobileAll(TEST_IMSI1)