Add callbacks for service offload

Components that can provide offload like IpClient (packet
filter offloading) can use the API to register a callback to be notified
when offload is necessary.

Bug: 269240366
Test: atest CtsNetTestCases
Change-Id: I8080702f5b530001b88e79e504f4722ac01bc576
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index ffa2857..9520ef6 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -82,6 +82,17 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+// The filegroup lists files that are necessary for verifying building mdns as a standalone,
+// for use with service-connectivity-mdns-standalone-build-test
+filegroup {
+    name: "framework-connectivity-t-mdns-standalone-build-sources",
+    srcs: [
+        "src/android/net/nsd/OffloadEngine.java",
+        "src/android/net/nsd/OffloadServiceInfo.java",
+    ],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index 5533154..e671db1 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -17,6 +17,7 @@
 package android.net.nsd;
 
 import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.IOffloadEngine;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Messenger;
 
@@ -35,4 +36,6 @@
     void stopResolution(int listenerKey);
     void registerServiceInfoCallback(int listenerKey, in NsdServiceInfo serviceInfo);
     void unregisterServiceInfoCallback(int listenerKey);
+    void registerOffloadEngine(String ifaceName, in IOffloadEngine cb, long offloadCapabilities, long offloadType);
+    void unregisterOffloadEngine(in IOffloadEngine cb);
 }
\ No newline at end of file
diff --git a/framework-t/src/android/net/nsd/IOffloadEngine.aidl b/framework-t/src/android/net/nsd/IOffloadEngine.aidl
new file mode 100644
index 0000000..379c2e0
--- /dev/null
+++ b/framework-t/src/android/net/nsd/IOffloadEngine.aidl
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.net.nsd.OffloadServiceInfo;
+
+/**
+ * Callbacks from NsdService to inform providers of packet offload.
+ * @hide
+ */
+oneway interface IOffloadEngine {
+    void onOffloadServiceUpdated(in OffloadServiceInfo info);
+    void onOffloadServiceRemoved(in OffloadServiceInfo info);
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 2930cbd..934f185 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,6 +16,9 @@
 
 package android.net.nsd;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
+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;
 
@@ -45,9 +48,11 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -246,6 +251,10 @@
     public static final int UNREGISTER_SERVICE_CALLBACK             = 31;
     /** @hide */
     public static final int UNREGISTER_SERVICE_CALLBACK_SUCCEEDED   = 32;
+    /** @hide */
+    public static final int REGISTER_OFFLOAD_ENGINE                 = 33;
+    /** @hide */
+    public static final int UNREGISTER_OFFLOAD_ENGINE               = 34;
 
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -313,8 +322,107 @@
     private final ArrayMap<Integer, PerNetworkDiscoveryTracker>
             mPerNetworkDiscoveryMap = new ArrayMap<>();
 
+    @GuardedBy("mOffloadEngines")
+    private final ArrayList<OffloadEngineProxy> mOffloadEngines = new ArrayList<>();
     private final ServiceHandler mHandler;
 
+    private static class OffloadEngineProxy extends IOffloadEngine.Stub {
+        private final Executor mExecutor;
+        private final OffloadEngine mEngine;
+
+        private OffloadEngineProxy(@NonNull Executor executor, @NonNull OffloadEngine appCb) {
+            mExecutor = executor;
+            mEngine = appCb;
+        }
+
+        @Override
+        public void onOffloadServiceUpdated(OffloadServiceInfo info) {
+            mExecutor.execute(() -> mEngine.onOffloadServiceUpdated(info));
+        }
+
+        @Override
+        public void onOffloadServiceRemoved(OffloadServiceInfo info) {
+            mExecutor.execute(() -> mEngine.onOffloadServiceRemoved(info));
+        }
+    }
+
+    /**
+     * Registers an OffloadEngine with NsdManager.
+     *
+     * A caller can register itself as an OffloadEngine if it supports mDns hardware offload.
+     * The caller must implement the {@link OffloadEngine} interface and update hardware offload
+     * state property when the {@link OffloadEngine#onOffloadServiceUpdated} and
+     * {@link OffloadEngine#onOffloadServiceRemoved} callback are called. Multiple engines may be
+     * registered for the same interface, and that the same engine cannot be registered twice.
+     *
+     * @param ifaceName  indicates which network interface the hardware offload runs on
+     * @param offloadType    the type of offload that the offload engine support
+     * @param offloadCapability    the capabilities of the offload engine
+     * @param executor   the executor on which to receive the offload callbacks
+     * @param engine     the OffloadEngine that will receive the offload callbacks
+     * @throws IllegalStateException if the engine is already registered.
+     *
+     * @hide
+     */
+    //@SystemApi
+    @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
+            NETWORK_STACK})
+    public void registerOffloadEngine(@NonNull String ifaceName,
+            @OffloadEngine.OffloadType long offloadType,
+            @OffloadEngine.OffloadCapability long offloadCapability, @NonNull Executor executor,
+            @NonNull OffloadEngine engine) {
+        Objects.requireNonNull(ifaceName);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(engine);
+        final OffloadEngineProxy cbImpl = new OffloadEngineProxy(executor, engine);
+        synchronized (mOffloadEngines) {
+            if (CollectionUtils.contains(mOffloadEngines, impl -> impl.mEngine == engine)) {
+                throw new IllegalStateException("This engine is already registered");
+            }
+            mOffloadEngines.add(cbImpl);
+        }
+        try {
+            mService.registerOffloadEngine(ifaceName, cbImpl, offloadCapability, offloadType);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Unregisters an OffloadEngine from NsdService.
+     *
+     * A caller can unregister itself as an OffloadEngine when it doesn't want to receive the
+     * callback anymore. The OffloadEngine must have been previously registered with the system
+     * using the {@link NsdManager#registerOffloadEngine} method.
+     *
+     * @param engine OffloadEngine object to be removed from NsdService
+     * @throws IllegalStateException if the engine is not registered.
+     *
+     * @hide
+     */
+    //@SystemApi
+    @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
+            NETWORK_STACK})
+    public void unregisterOffloadEngine(@NonNull OffloadEngine engine) {
+        Objects.requireNonNull(engine);
+        final OffloadEngineProxy cbImpl;
+        synchronized (mOffloadEngines) {
+            final int index = CollectionUtils.indexOf(mOffloadEngines,
+                    impl -> impl.mEngine == engine);
+            if (index < 0) {
+                throw new IllegalStateException("This engine is not registered");
+            }
+            cbImpl = mOffloadEngines.remove(index);
+        }
+
+        try {
+            mService.unregisterOffloadEngine(cbImpl);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     private class PerNetworkDiscoveryTracker {
         final String mServiceType;
         final int mProtocolType;
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
new file mode 100644
index 0000000..d939725
--- /dev/null
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * OffloadEngine is an interface for mDns hardware offloading.
+ *
+ * An offloading engine can interact with the firmware code to instruct the hardware to
+ * offload some of mDns network traffic before it reached android OS. This can improve the
+ * power consumption performance of the host system by not always waking up the OS to handle
+ * the mDns packet when the device is in low power mode.
+ *
+ * @hide
+ */
+//@SystemApi
+public interface OffloadEngine {
+    /**
+     * Indicates that the OffloadEngine can generate replies to mDns queries.
+     *
+     * @see OffloadServiceInfo#getOffloadPayload()
+     */
+    int OFFLOAD_TYPE_REPLY = 1;
+    /**
+     * Indicates that the OffloadEngine can filter and drop mDns queries.
+     */
+    int OFFLOAD_TYPE_FILTER_QUERIES = 1 << 1;
+    /**
+     * Indicates that the OffloadEngine can filter and drop mDns replies. It can allow mDns packets
+     * to be received even when no app holds a {@link android.net.wifi.WifiManager.MulticastLock}.
+     */
+    int OFFLOAD_TYPE_FILTER_REPLIES = 1 << 2;
+
+    /**
+     * Indicates that the OffloadEngine can bypass multicast lock.
+     */
+    int OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK = 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"OFFLOAD_TYPE"}, value = {
+            OFFLOAD_TYPE_REPLY,
+            OFFLOAD_TYPE_FILTER_QUERIES,
+            OFFLOAD_TYPE_FILTER_REPLIES,
+    })
+    @interface OffloadType {}
+
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"OFFLOAD_CAPABILITY"}, value = {
+            OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK
+    })
+    @interface OffloadCapability {}
+
+    /**
+     * To be called when the OffloadServiceInfo is added or updated.
+     *
+     * @param info The OffloadServiceInfo to add or update.
+     */
+    void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info);
+
+    /**
+     * To be called when the OffloadServiceInfo is removed.
+     *
+     * @param info The OffloadServiceInfo to remove.
+     */
+    void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info);
+}
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
new file mode 100644
index 0000000..4aec720
--- /dev/null
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The OffloadServiceInfo class contains all the necessary information the OffloadEngine needs to
+ * know about how to offload an mDns service. The OffloadServiceInfo is keyed on
+ * {@link OffloadServiceInfo.Key} which is a (serviceName, serviceType) pair.
+ *
+ * @hide
+ */
+// @SystemApi
+public final class OffloadServiceInfo implements Parcelable {
+    @NonNull
+    private final Key mKey;
+    @NonNull
+    private final String mHostname;
+    @NonNull final List<String> mSubtypes;
+    @Nullable
+    private final byte[] mOffloadPayload;
+    private final int mPriority;
+    private final long mOffloadType;
+
+    /**
+     * Creates a new OffloadServiceInfo object with the specified parameters.
+     *
+     * @param key The key of the service.
+     * @param subtypes The list of subTypes of the service.
+     * @param hostname The name of the host offering the service. It is meaningful only when
+     *                 offloadType contains OFFLOAD_REPLY.
+     * @param offloadPayload The raw udp payload for hardware offloading.
+     * @param priority The priority of the service, @see #getPriority.
+     * @param offloadType The type of the service offload, @see #getOffloadType.
+     */
+    public OffloadServiceInfo(@NonNull Key key,
+            @NonNull List<String> subtypes, @NonNull String hostname,
+            @Nullable byte[] offloadPayload,
+            @IntRange(from = 0, to = Integer.MAX_VALUE) int priority,
+            @OffloadEngine.OffloadType long offloadType) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(subtypes);
+        Objects.requireNonNull(hostname);
+        mKey = key;
+        mSubtypes = subtypes;
+        mHostname = hostname;
+        mOffloadPayload = offloadPayload;
+        mPriority = priority;
+        mOffloadType = offloadType;
+    }
+
+    /**
+     * Creates a new OffloadServiceInfo object from a Parcel.
+     *
+     * @param in The Parcel to read the object from.
+     *
+     * @hide
+     */
+    public OffloadServiceInfo(@NonNull Parcel in) {
+        mKey = in.readParcelable(Key.class.getClassLoader(),
+                Key.class);
+        mSubtypes = in.createStringArrayList();
+        mHostname = in.readString();
+        mOffloadPayload = in.createByteArray();
+        mPriority = in.readInt();
+        mOffloadType = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mKey, flags);
+        dest.writeStringList(mSubtypes);
+        dest.writeString(mHostname);
+        dest.writeByteArray(mOffloadPayload);
+        dest.writeInt(mPriority);
+        dest.writeLong(mOffloadType);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<OffloadServiceInfo> CREATOR = new Creator<OffloadServiceInfo>() {
+        @Override
+        public OffloadServiceInfo createFromParcel(Parcel in) {
+            return new OffloadServiceInfo(in);
+        }
+
+        @Override
+        public OffloadServiceInfo[] newArray(int size) {
+            return new OffloadServiceInfo[size];
+        }
+    };
+
+    /**
+     * Get the {@link Key}.
+     */
+    @NonNull
+    public Key getKey() {
+        return mKey;
+    }
+
+    /**
+     * Get the host name. (e.g. "Android.local" )
+     */
+    @NonNull
+    public String getHostname() {
+        return mHostname;
+    }
+
+    /**
+     * Get the service subtypes. (e.g. ["_ann"] )
+     */
+    @NonNull
+    public List<String> getSubtypes() {
+        return Collections.unmodifiableList(mSubtypes);
+    }
+
+    /**
+     * Get the raw udp payload that the OffloadEngine can use to directly reply the incoming query.
+     * <p>
+     * It is null if the OffloadEngine can not handle transmit. The packet must be sent as-is when
+     * replying to query.
+     */
+    @Nullable
+    public byte[] getOffloadPayload() {
+        if (mOffloadPayload == null) {
+            return null;
+        } else {
+            return mOffloadPayload.clone();
+        }
+    }
+
+    /**
+     * Get the offloadType.
+     * <p>
+     * For example, if the {@link com.android.server.NsdService} requests the OffloadEngine to both
+     * filter the mDNS queries and replies, the {@link #mOffloadType} =
+     * ({@link OffloadEngine#OFFLOAD_TYPE_FILTER_QUERIES} |
+     * {@link OffloadEngine#OFFLOAD_TYPE_FILTER_REPLIES}).
+     */
+    @OffloadEngine.OffloadType public long getOffloadType() {
+        return mOffloadType;
+    }
+
+    /**
+     * Get the priority for the OffloadServiceInfo.
+     * <p>
+     * When OffloadEngine don't have enough resource
+     * (e.g. not enough memory) to offload all the OffloadServiceInfo. The OffloadServiceInfo
+     * having lower priority values should be handled by the OffloadEngine first.
+     */
+    public int getPriority() {
+        return mPriority;
+    }
+
+    /**
+     * Only for debug purpose, the string can be long as the raw packet is dump in the string.
+     */
+    @Override
+    public String toString() {
+        return String.format(
+                "OffloadServiceInfo{ mOffloadServiceInfoKey=%s, mHostName=%s, "
+                        + "mOffloadPayload=%s, mPriority=%d, mOffloadType=%d, mSubTypes=%s }",
+                mKey,
+                mHostname, HexDump.dumpHexString(mOffloadPayload), mPriority,
+                mOffloadType, mSubtypes.toString());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof OffloadServiceInfo)) return false;
+        OffloadServiceInfo that = (OffloadServiceInfo) o;
+        return mPriority == that.mPriority && mOffloadType == that.mOffloadType
+                && mKey.equals(that.mKey)
+                && mHostname.equals(
+                that.mHostname) && Arrays.equals(mOffloadPayload,
+                that.mOffloadPayload)
+                && mSubtypes.equals(that.mSubtypes);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(mKey, mHostname, mPriority,
+                mOffloadType, mSubtypes);
+        result = 31 * result + Arrays.hashCode(mOffloadPayload);
+        return result;
+    }
+
+    /**
+     * The {@link OffloadServiceInfo.Key} is the (serviceName, serviceType) pair.
+     */
+    public static final class Key implements Parcelable {
+        @NonNull
+        private final String mServiceName;
+        @NonNull
+        private final String mServiceType;
+
+        /**
+         * Creates a new OffloadServiceInfoKey object with the specified parameters.
+         *
+         * @param serviceName The name of the service.
+         * @param serviceType The type of the service.
+         */
+        public Key(@NonNull String serviceName, @NonNull String serviceType) {
+            Objects.requireNonNull(serviceName);
+            Objects.requireNonNull(serviceType);
+            mServiceName = serviceName;
+            mServiceType = serviceType;
+        }
+
+        /**
+         * Creates a new OffloadServiceInfoKey object from a Parcel.
+         *
+         * @param in The Parcel to read the object from.
+         *
+         * @hide
+         */
+        public Key(@NonNull Parcel in) {
+            mServiceName = in.readString();
+            mServiceType = in.readString();
+        }
+        /**
+         * Get the service name. (e.g. "NsdChat")
+         */
+        @NonNull
+        public String getServiceName() {
+            return mServiceName;
+        }
+
+        /**
+         * Get the service type. (e.g. "_http._tcp.local" )
+         */
+        @NonNull
+        public String getServiceType() {
+            return mServiceType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeString(mServiceName);
+            dest.writeString(mServiceType);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @NonNull
+        public static final Creator<Key> CREATOR =
+                new Creator<Key>() {
+            @Override
+            public Key createFromParcel(Parcel in) {
+                return new Key(in);
+            }
+
+            @Override
+            public Key[] newArray(int size) {
+                return new Key[size];
+            }
+        };
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Key)) return false;
+            Key that = (Key) o;
+            return Objects.equals(mServiceName, that.mServiceName) && Objects.equals(
+                    mServiceType, that.mServiceType);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mServiceName, mServiceType);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("OffloadServiceInfoKey{ mServiceName=%s, mServiceType=%s }",
+                    mServiceName, mServiceType);
+        }
+    }
+}
diff --git a/framework/aidl-export/android/net/nsd/OffloadServiceInfo.aidl b/framework/aidl-export/android/net/nsd/OffloadServiceInfo.aidl
new file mode 100644
index 0000000..aa7aef2
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/OffloadServiceInfo.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.net.nsd;
+
+@JavaOnlyStableParcelable parcelable OffloadServiceInfo;
\ No newline at end of file
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index 9b48d57..1ac5e8e 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -27,4 +27,10 @@
 # Don't touch anything that's already under android.net.http (cronet)
 # This is required since android.net.http contains api classes and hidden classes.
 # TODO: Remove this after hidden classes are moved to different package
-android\.net\.http\..+
\ No newline at end of file
+android\.net\.http\..+
+
+# TODO: OffloadServiceInfo is being added as an API, but wasn't an API yet in the first module
+# versions targeting U. Do not jarjar it such versions so that tests do not have to cover both
+# cases. This will be removed in an upcoming change marking it as API.
+android\.net\.nsd\.OffloadServiceInfo(\$.+)?
+android\.net\.nsd\.OffloadEngine(\$.+)?
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 7de749c..c277cf6 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -88,8 +88,9 @@
     name: "service-connectivity-mdns-standalone-build-test",
     sdk_version: "core_platform",
     srcs: [
-        ":service-mdns-droidstubs",
         "src/com/android/server/connectivity/mdns/**/*.java",
+        ":framework-connectivity-t-mdns-standalone-build-sources",
+        ":service-mdns-droidstubs"
     ],
     exclude_srcs: [
         "src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 745c5bc..1a05d46 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.ConnectivityManager.NETID_UNSET;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -46,9 +47,12 @@
 import android.net.nsd.INsdManager;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.IOffloadEngine;
 import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Handler;
@@ -56,6 +60,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
@@ -98,6 +103,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -215,6 +221,24 @@
     // The number of client that ever connected.
     private int mClientNumberId = 1;
 
+    private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
+            new RemoteCallbackList<>();
+
+    private static class OffloadEngineInfo {
+        @NonNull final String mInterfaceName;
+        final long mOffloadCapabilities;
+        final long mOffloadType;
+        @NonNull final IOffloadEngine mOffloadEngine;
+
+        OffloadEngineInfo(@NonNull IOffloadEngine offloadEngine,
+                @NonNull String interfaceName, long capabilities, long offloadType) {
+            this.mOffloadEngine = offloadEngine;
+            this.mInterfaceName = interfaceName;
+            this.mOffloadCapabilities = capabilities;
+            this.mOffloadType = offloadType;
+        }
+    }
+
     private static class MdnsListener implements MdnsServiceBrowserListener {
         protected final int mClientRequestId;
         protected final int mTransactionId;
@@ -719,6 +743,7 @@
                 final int transactionId;
                 final int clientRequestId = msg.arg2;
                 final ListenerArgs args;
+                final OffloadEngineInfo offloadEngineInfo;
                 switch (msg.what) {
                     case NsdManager.DISCOVER_SERVICES: {
                         if (DBG) Log.d(TAG, "Discover services");
@@ -1114,6 +1139,16 @@
                             return NOT_HANDLED;
                         }
                         break;
+                    case NsdManager.REGISTER_OFFLOAD_ENGINE:
+                        offloadEngineInfo = (OffloadEngineInfo) msg.obj;
+                        // TODO: Limits the number of registrations created by a given class.
+                        mOffloadEngines.register(offloadEngineInfo.mOffloadEngine,
+                                offloadEngineInfo);
+                        // TODO: Sends all the existing OffloadServiceInfos back.
+                        break;
+                    case NsdManager.UNREGISTER_OFFLOAD_ENGINE:
+                        mOffloadEngines.unregister((IOffloadEngine) msg.obj);
+                        break;
                     default:
                         return NOT_HANDLED;
                 }
@@ -1771,7 +1806,42 @@
         }
     }
 
+    private void sendOffloadServiceInfosUpdate(@NonNull String targetInterfaceName,
+            @NonNull OffloadServiceInfo offloadServiceInfo, boolean isRemove) {
+        final int count = mOffloadEngines.beginBroadcast();
+        try {
+            for (int i = 0; i < count; i++) {
+                final OffloadEngineInfo offloadEngineInfo =
+                        (OffloadEngineInfo) mOffloadEngines.getBroadcastCookie(i);
+                final String interfaceName = offloadEngineInfo.mInterfaceName;
+                if (!targetInterfaceName.equals(interfaceName)
+                        || ((offloadEngineInfo.mOffloadType
+                        & offloadServiceInfo.getOffloadType()) == 0)) {
+                    continue;
+                }
+                try {
+                    if (isRemove) {
+                        mOffloadEngines.getBroadcastItem(i).onOffloadServiceRemoved(
+                                offloadServiceInfo);
+                    } else {
+                        mOffloadEngines.getBroadcastItem(i).onOffloadServiceUpdated(
+                                offloadServiceInfo);
+                    }
+                } catch (RemoteException e) {
+                    // Can happen in regular cases, do not log a stacktrace
+                    Log.i(TAG, "Failed to send offload callback, remote died", e);
+                }
+            }
+        } finally {
+            mOffloadEngines.finishBroadcast();
+        }
+    }
+
     private class AdvertiserCallback implements MdnsAdvertiser.AdvertiserCallback {
+        // TODO: add a callback to notify when a service is being added on each interface (as soon
+        // as probing starts), and call mOffloadCallbacks. This callback is for
+        // OFFLOAD_CAPABILITY_FILTER_REPLIES offload type.
+
         @Override
         public void onRegisterServiceSucceeded(int transactionId, NsdServiceInfo registeredInfo) {
             mServiceLogs.log("onRegisterServiceSucceeded: transactionId " + transactionId);
@@ -1801,6 +1871,18 @@
                     request.calculateRequestDurationMs());
         }
 
+        @Override
+        public void onOffloadStartOrUpdate(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo) {
+            sendOffloadServiceInfosUpdate(interfaceName, offloadServiceInfo, false /* isRemove */);
+        }
+
+        @Override
+        public void onOffloadStop(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo) {
+            sendOffloadServiceInfosUpdate(interfaceName, offloadServiceInfo, true /* isRemove */);
+        }
+
         private ClientInfo getClientInfoOrLog(int transactionId) {
             final ClientInfo clientInfo = mTransactionIdToClientInfoMap.get(transactionId);
             if (clientInfo == null) {
@@ -1920,6 +2002,32 @@
         public void binderDied() {
             mNsdStateMachine.sendMessage(
                     mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_CLIENT, this));
+
+        }
+
+        @Override
+        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);
+            Objects.requireNonNull(ifaceName);
+            Objects.requireNonNull(cb);
+            mNsdStateMachine.sendMessage(
+                    mNsdStateMachine.obtainMessage(NsdManager.REGISTER_OFFLOAD_ENGINE,
+                            new OffloadEngineInfo(cb, ifaceName, offloadCapabilities,
+                                    offloadTypes)));
+        }
+
+        @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);
+            Objects.requireNonNull(cb);
+            mNsdStateMachine.sendMessage(
+                    mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_OFFLOAD_ENGINE, cb));
         }
     }
 
@@ -2003,25 +2111,41 @@
             return IFACE_IDX_ANY;
         }
 
-        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-        if (cm == null) {
-            Log.wtf(TAG, "No ConnectivityManager for resolveService");
+        String interfaceName = getNetworkInterfaceName(network);
+        if (interfaceName == null) {
             return IFACE_IDX_ANY;
         }
-        final LinkProperties lp = cm.getLinkProperties(network);
-        if (lp == null) return IFACE_IDX_ANY;
+        return getNetworkInterfaceIndexByName(interfaceName);
+    }
 
+    private String getNetworkInterfaceName(@Nullable Network network) {
+        if (network == null) {
+            return null;
+        }
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        if (cm == null) {
+            Log.wtf(TAG, "No ConnectivityManager");
+            return null;
+        }
+        final LinkProperties lp = cm.getLinkProperties(network);
+        if (lp == null) {
+            return null;
+        }
         // Only resolve on non-stacked interfaces
+        return lp.getInterfaceName();
+    }
+
+    private int getNetworkInterfaceIndexByName(final String ifaceName) {
         final NetworkInterface iface;
         try {
-            iface = NetworkInterface.getByName(lp.getInterfaceName());
+            iface = NetworkInterface.getByName(ifaceName);
         } catch (SocketException e) {
             Log.e(TAG, "Error querying interface", e);
             return IFACE_IDX_ANY;
         }
 
         if (iface == null) {
-            Log.e(TAG, "Interface not found: " + lp.getInterfaceName());
+            Log.e(TAG, "Interface not found: " + ifaceName);
             return IFACE_IDX_ANY;
         }
 
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 158d7a3..1bc059d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -24,15 +24,19 @@
 import android.net.Network;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
 import android.os.Looper;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -68,9 +72,10 @@
             new ArrayMap<>();
     private final SparseArray<Registration> mRegistrations = new SparseArray<>();
     private final Dependencies mDeps;
-
     private String[] mDeviceHostName;
     @NonNull private final SharedLog mSharedLog;
+    private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices =
+            new ArrayMap<>();
 
     /**
      * Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -115,18 +120,32 @@
     private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
             new MdnsInterfaceAdvertiser.Callback() {
         @Override
-        public void onRegisterServiceSucceeded(
+        public void onServiceProbingSucceeded(
                 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
-            // Wait for all current interfaces to be done probing before notifying of success.
-            if (any(mAllAdvertisers, (k, a) -> a.isProbing(serviceId))) return;
-            // The service may still be unregistered/renamed if a conflict is found on a later added
-            // interface, or if a conflicting announcement/reply is detected (RFC6762 9.)
-
             final Registration registration = mRegistrations.get(serviceId);
             if (registration == null) {
                 Log.wtf(TAG, "Register succeeded for unknown registration");
                 return;
             }
+
+            final String interfaceName = advertiser.getSocketInterfaceName();
+            final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                    mInterfaceOffloadServices.computeIfAbsent(
+                            interfaceName, k -> new ArrayList<>());
+            // Remove existing offload services from cache for update.
+            existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
+            final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
+                    serviceId,
+                    registration);
+            existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
+            mCb.onOffloadStartOrUpdate(interfaceName,
+                    newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+
+            // Wait for all current interfaces to be done probing before notifying of success.
+            if (any(mAllAdvertisers, (k, a) -> a.isProbing(serviceId))) return;
+            // The service may still be unregistered/renamed if a conflict is found on a later added
+            // interface, or if a conflicting announcement/reply is detected (RFC6762 9.)
+
             if (!registration.mNotifiedRegistrationSuccess) {
                 mCb.onRegisterServiceSucceeded(serviceId, registration.getServiceInfo());
                 registration.mNotifiedRegistrationSuccess = true;
@@ -148,7 +167,12 @@
                 registration.mNotifiedRegistrationSuccess = false;
 
                 // The service was done probing, just reset it to probing state (RFC6762 9.)
-                forAllAdvertisers(a -> a.restartProbingForConflict(serviceId));
+                forAllAdvertisers(a -> {
+                    if (!a.maybeRestartProbingForConflict(serviceId)) {
+                        return;
+                    }
+                    maybeSendOffloadStop(a.getSocketInterfaceName(), serviceId);
+                });
                 return;
             }
 
@@ -196,6 +220,22 @@
         registration.updateForConflict(newInfo, renameCount);
     }
 
+    private void maybeSendOffloadStop(final String interfaceName, int serviceId) {
+        final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+                mInterfaceOffloadServices.get(interfaceName);
+        if (existingOffloadServiceInfoWrappers == null) {
+            return;
+        }
+        // Stop the offloaded service by matching the service id
+        int idx = CollectionUtils.indexOf(existingOffloadServiceInfoWrappers,
+                item -> item.mServiceId == serviceId);
+        if (idx >= 0) {
+            mCb.onOffloadStop(interfaceName,
+                    existingOffloadServiceInfoWrappers.get(idx).mOffloadServiceInfo);
+            existingOffloadServiceInfoWrappers.remove(idx);
+        }
+    }
+
     /**
      * A request for a {@link MdnsInterfaceAdvertiser}.
      *
@@ -221,7 +261,22 @@
          * @return true if this {@link InterfaceAdvertiserRequest} should now be deleted.
          */
         boolean onAdvertiserDestroyed(@NonNull MdnsInterfaceSocket socket) {
-            mAdvertisers.remove(socket);
+            final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket);
+            if (removedAdvertiser != null) {
+                final String interfaceName = removedAdvertiser.getSocketInterfaceName();
+                // If the interface is destroyed, stop all hardware offloading on that interface.
+                final List<OffloadServiceInfoWrapper> offloadServiceInfoWrappers =
+                        mInterfaceOffloadServices.remove(
+                                interfaceName);
+                if (offloadServiceInfoWrappers != null) {
+                    for (OffloadServiceInfoWrapper offloadServiceInfoWrapper :
+                            offloadServiceInfoWrappers) {
+                        mCb.onOffloadStop(interfaceName,
+                                offloadServiceInfoWrapper.mOffloadServiceInfo);
+                    }
+                }
+            }
+
             if (mAdvertisers.size() == 0 && mPendingRegistrations.size() == 0) {
                 // No advertiser is using sockets from this request anymore (in particular for exit
                 // announcements), and there is no registration so newer sockets will not be
@@ -282,7 +337,10 @@
         void removeService(int id) {
             mPendingRegistrations.remove(id);
             for (int i = 0; i < mAdvertisers.size(); i++) {
-                mAdvertisers.valueAt(i).removeService(id);
+                final MdnsInterfaceAdvertiser advertiser = mAdvertisers.valueAt(i);
+                advertiser.removeService(id);
+
+                maybeSendOffloadStop(advertiser.getSocketInterfaceName(), id);
             }
         }
 
@@ -325,6 +383,16 @@
         }
     }
 
+    private static class OffloadServiceInfoWrapper {
+        private final @NonNull OffloadServiceInfo mOffloadServiceInfo;
+        private final int mServiceId;
+
+        OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo) {
+            mOffloadServiceInfo = offloadServiceInfo;
+            mServiceId = serviceId;
+        }
+    }
+
     private static class Registration {
         @NonNull
         final String mOriginalName;
@@ -425,6 +493,24 @@
 
         // Unregistration is notified immediately as success in NsdService so no callback is needed
         // here.
+
+        /**
+         * Called when a service is ready to be sent for hardware offloading.
+         *
+         * @param interfaceName the interface for sending the update to.
+         * @param offloadServiceInfo the offloading content.
+         */
+        void onOffloadStartOrUpdate(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo);
+
+        /**
+         * Called when a service is removed or the MdnsInterfaceAdvertiser is destroyed.
+         *
+         * @param interfaceName the interface for sending the update to.
+         * @param offloadServiceInfo the offloading content.
+         */
+        void onOffloadStop(@NonNull String interfaceName,
+                @NonNull OffloadServiceInfo offloadServiceInfo);
     }
 
     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@@ -525,4 +611,28 @@
             return false;
         });
     }
+
+    private OffloadServiceInfoWrapper createOffloadService(int serviceId,
+            @NonNull Registration registration) {
+        final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
+        List<String> subTypes = new ArrayList<>();
+        String subType = registration.getSubtype();
+        if (subType != null) {
+            subTypes.add(subType);
+        }
+        final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
+                new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
+                        nsdServiceInfo.getServiceType()),
+                subTypes,
+                String.join(".", mDeviceHostName),
+                null /* rawOffloadPacket */,
+                // TODO: define overlayable resources in
+                // ServiceConnectivityResources that set the priority based on
+                // service type.
+                0 /* priority */,
+                // TODO: set the offloadType based on the callback timing.
+                OffloadEngine.OFFLOAD_TYPE_REPLY);
+        return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
+    }
+
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 724a704..c5177b7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -73,7 +73,7 @@
         /**
          * Called by the advertiser after it successfully registered a service, after probing.
          */
-        void onRegisterServiceSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+        void onServiceProbingSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
 
         /**
          * Called by the advertiser when a conflict was found, during or after probing.
@@ -101,7 +101,7 @@
         public void onFinished(MdnsProber.ProbingInfo info) {
             final MdnsAnnouncer.AnnouncementInfo announcementInfo;
             mSharedLog.i("Probing finished for service " + info.getServiceId());
-            mCbHandler.post(() -> mCb.onRegisterServiceSucceeded(
+            mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
                     MdnsInterfaceAdvertiser.this, info.getServiceId()));
             try {
                 announcementInfo = mRecordRepository.onProbingSucceeded(info);
@@ -282,11 +282,12 @@
     /**
      * Reset a service to the probing state due to a conflict found on the network.
      */
-    public void restartProbingForConflict(int serviceId) {
+    public boolean maybeRestartProbingForConflict(int serviceId) {
         final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
-        if (probingInfo == null) return;
+        if (probingInfo == null) return false;
 
         mProber.restartForConflict(probingInfo);
+        return true;
     }
 
     /**
@@ -346,4 +347,8 @@
         if (answers == null) return;
         mReplySender.queueReply(answers);
     }
+
+    public String getSocketInterfaceName() {
+        return mSocket.getInterface().getName();
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 49620b0..7731ac9 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -16,6 +16,7 @@
 package android.net.cts
 
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.app.compat.CompatChanges
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -60,6 +61,8 @@
 import android.net.nsd.NsdManager.RegistrationListener
 import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
@@ -353,6 +356,22 @@
         }
     }
 
+    private class TestNsdOffloadEngine : OffloadEngine,
+        NsdRecord<TestNsdOffloadEngine.OffloadEvent>() {
+        sealed class OffloadEvent : NsdEvent {
+            data class AddOrUpdateEvent(val info: OffloadServiceInfo) : OffloadEvent()
+            data class RemoveEvent(val info: OffloadServiceInfo) : OffloadEvent()
+        }
+
+        override fun onOffloadServiceUpdated(info: OffloadServiceInfo) {
+            add(OffloadEvent.AddOrUpdateEvent(info))
+        }
+
+        override fun onOffloadServiceRemoved(info: OffloadServiceInfo) {
+            add(OffloadEvent.RemoveEvent(info))
+        }
+    }
+
     @Before
     fun setUp() {
         handlerThread.start()
@@ -858,6 +877,52 @@
         }
     }
 
+    fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo) {
+        assertEquals(serviceName, serviceInfo.key.serviceName)
+        assertEquals(serviceType, serviceInfo.key.serviceType)
+        assertEquals(listOf<String>("_subtype"), serviceInfo.subtypes)
+        assertTrue(serviceInfo.hostname.startsWith("Android_"))
+        assertTrue(serviceInfo.hostname.endsWith("local"))
+        assertEquals(0, serviceInfo.priority)
+        assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
+    }
+
+    @Test
+    fun testNsdManager_registerOffloadEngine() {
+        val targetSdkVersion = context.packageManager
+            .getTargetSdkVersion(context.applicationInfo.packageName)
+        // The offload callbacks are only supported with the new backend,
+        // enabled with target SDK U+.
+        assumeTrue(isAtLeastU() || targetSdkVersion > Build.VERSION_CODES.TIRAMISU)
+        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
+            .expectCallback<TestNsdOffloadEngine.OffloadEvent.AddOrUpdateEvent>()
+        checkOffloadServiceInfo(addOrUpdateEvent.info)
+
+        nsdManager.unregisterService(record)
+        val unregisterEvent = offloadEngine
+            .expectCallback<TestNsdOffloadEngine.OffloadEvent.RemoveEvent>()
+        checkOffloadServiceInfo(unregisterEvent.info)
+
+        runAsShell(NETWORK_SETTINGS) {
+            nsdManager.unregisterOffloadEngine(offloadEngine)
+        }
+    }
+
     private fun checkConnectSocketToMdnsd(shouldFail: Boolean) {
         val discoveryRecord = NsdDiscoveryRecord()
         val localSocket = LocalSocket()
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 6a0334f..9b38fea 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -20,6 +20,8 @@
 import android.net.LinkAddress
 import android.net.Network
 import android.net.nsd.NsdServiceInfo
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
@@ -60,6 +62,8 @@
 private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 private const val TEST_SUBTYPE = "_subtype"
+private val TEST_INTERFACE1 = "test_iface1"
+private val TEST_INTERFACE2 = "test_iface2"
 
 private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     port = 12345
@@ -94,6 +98,24 @@
         network = null
     }
 
+private val OFFLOAD_SERVICEINFO = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(TEST_SUBTYPE),
+    "Android_test.local",
+    null, /* rawOffloadPacket */
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
+private val OFFLOAD_SERVICEINFO_NO_SUBTYPE = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+    listOf(),
+    "Android_test.local",
+    null, /* rawOffloadPacket */
+    0, /* priority */
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsAdvertiserTest {
@@ -123,6 +145,8 @@
         doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
         doReturn(createEmptyNetworkInterface()).`when`(mockSocket1).getInterface()
         doReturn(createEmptyNetworkInterface()).`when`(mockSocket2).getInterface()
+        doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
+        doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
     }
 
     @After
@@ -160,12 +184,15 @@
         )
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
         postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         verify(mockInterfaceAdvertiser1).destroyNow()
+        postSync { intAdvCbCaptor.value.onDestroyed(mockSocket1) }
+        verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
     }
 
     @Test
@@ -195,14 +222,16 @@
                 anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor1.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
 
         // Need both advertisers to finish probing and call onRegisterServiceSucceeded
         verify(cb, never()).onRegisterServiceSucceeded(anyInt(), any())
         doReturn(false).`when`(mockInterfaceAdvertiser2).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor2.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor2.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser2, SERVICE_ID_1) }
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
                 argThat { it.matches(ALL_NETWORKS_SERVICE) })
 
@@ -210,6 +239,8 @@
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockInterfaceAdvertiser1).removeService(SERVICE_ID_1)
         verify(mockInterfaceAdvertiser2).removeService(SERVICE_ID_1)
+        verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
+        verify(cb).onOffloadStop(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
 
         // Interface advertisers call onDestroyed after sending exit announcements
         postSync { intAdvCbCaptor1.value.onDestroyed(mockSocket1) }
@@ -285,12 +316,12 @@
             argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_1) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
-        postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
+        postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
                 mockInterfaceAdvertiser1, SERVICE_ID_2) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_2),
                 argThat { it.matches(expectedRenamed) })
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index dd458b8..732ae2a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -150,7 +150,7 @@
                 0L /* initialDelayMs */)
 
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onRegisterServiceSucceeded(advertiser, TEST_SERVICE_ID_1)
+        verify(cb).onServiceProbingSucceeded(advertiser, TEST_SERVICE_ID_1)
 
         // Remove the service: expect exit announcements
         val testExitInfo = mock(ExitAnnouncementInfo::class.java)
@@ -256,7 +256,7 @@
         val mockProbingInfo = mock(ProbingInfo::class.java)
         doReturn(mockProbingInfo).`when`(repository).setServiceProbing(TEST_SERVICE_ID_1)
 
-        advertiser.restartProbingForConflict(TEST_SERVICE_ID_1)
+        advertiser.maybeRestartProbingForConflict(TEST_SERVICE_ID_1)
 
         verify(prober).restartForConflict(mockProbingInfo)
     }