Merge changes I6d0e5d34,I55af0796 into main

* changes:
  Inline tetherInterface and untetherInterface
  Separate route modification and update linkproperties
diff --git a/bpf/loader/Android.bp b/bpf/loader/Android.bp
index 780fe20..345e92b 100644
--- a/bpf/loader/Android.bp
+++ b/bpf/loader/Android.bp
@@ -42,6 +42,7 @@
     shared_libs: [
         "libbase",
         "liblog",
+        "libbpf",
     ],
     srcs: ["NetBpfLoad.cpp"],
     apex_available: [
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index bad506f..b9ef766 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -17,6 +17,7 @@
 #define LOG_TAG "NetBpfLoad"
 
 #include <arpa/inet.h>
+#include <bpf/libbpf.h>
 #include <dirent.h>
 #include <elf.h>
 #include <errno.h>
@@ -1438,10 +1439,12 @@
     if (isAtLeastV) ++bpfloader_ver;     // [46] BPFLOADER_MAINLINE_V_VERSION
     if (isAtLeast25Q2) ++bpfloader_ver;  // [47] BPFLOADER_MAINLINE_25Q2_VERSION
 
-    ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+    ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) libbpf: v%u.%u "
+          "uid:%d rc:%d%d",
           bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
-          kernelVersion(), describeArch(), getuid(),
-          has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+          kernelVersion(), describeArch(), libbpf_major_version(),
+          libbpf_minor_version(), getuid(), has_platform_bpfloader_rc,
+          has_platform_netbpfload_rc);
 
     if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
         ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
diff --git a/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
new file mode 100644
index 0000000..21af1a1
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+
+import com.android.net.module.util.HandlerUtils;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+public class DiscoveryExecutor implements Executor {
+    private static final String TAG = DiscoveryExecutor.class.getSimpleName();
+    @Nullable
+    private final HandlerThread mHandlerThread;
+
+    @GuardedBy("mPendingTasks")
+    @Nullable
+    private Handler mHandler;
+    // Store pending tasks and associated delay time. Each Pair represents a pending task
+    // (first) and its delay time (second).
+    @GuardedBy("mPendingTasks")
+    @NonNull
+    private final ArrayList<Pair<Runnable, Long>> mPendingTasks = new ArrayList<>();
+
+    DiscoveryExecutor(@Nullable Looper defaultLooper) {
+        if (defaultLooper != null) {
+            this.mHandlerThread = null;
+            synchronized (mPendingTasks) {
+                this.mHandler = new Handler(defaultLooper);
+            }
+        } else {
+            this.mHandlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+                @Override
+                protected void onLooperPrepared() {
+                    synchronized (mPendingTasks) {
+                        mHandler = new Handler(getLooper());
+                        for (Pair<Runnable, Long> pendingTask : mPendingTasks) {
+                            mHandler.postDelayed(pendingTask.first, pendingTask.second);
+                        }
+                        mPendingTasks.clear();
+                    }
+                }
+            };
+            this.mHandlerThread.start();
+        }
+    }
+
+    /**
+     * Check if the current thread is the expected thread. If it is, run the given function.
+     * Otherwise, execute it using the handler.
+     */
+    public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+        if (this.mHandlerThread == null) {
+            // Callers are expected to already be running on the handler when a defaultLooper
+            // was provided
+            function.run();
+        } else {
+            execute(function);
+        }
+    }
+
+    /** Execute the given function */
+    @Override
+    public void execute(Runnable function) {
+        executeDelayed(function, 0L /* delayMillis */);
+    }
+
+    /** Execute the given function after the specified amount of time elapses. */
+    public void executeDelayed(Runnable function, long delayMillis) {
+        final Handler handler;
+        synchronized (mPendingTasks) {
+            if (this.mHandler == null) {
+                mPendingTasks.add(Pair.create(function, delayMillis));
+                return;
+            } else {
+                handler = this.mHandler;
+            }
+        }
+        handler.postDelayed(function, delayMillis);
+    }
+
+    /** Shutdown the thread if necessary. */
+    public void shutDown() {
+        if (this.mHandlerThread != null) {
+            this.mHandlerThread.quitSafely();
+        }
+    }
+
+    /**
+     * Ensures that the current running thread is the same as the handler thread.
+     */
+    public void ensureRunningOnHandlerThread() {
+        synchronized (mPendingTasks) {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+        }
+    }
+
+    /**
+     * Runs the specified task synchronously for dump method.
+     */
+    public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
+        final Handler handler;
+        synchronized (mPendingTasks) {
+            if (this.mHandler == null) {
+                Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
+                return;
+            } else {
+                handler = this.mHandler;
+            }
+        }
+        HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
+    }
+}
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 c3306bd..bd00b70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -35,6 +35,7 @@
 import android.os.Looper;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -93,6 +94,7 @@
             new ArrayMap<>();
     private final MdnsFeatureFlags mMdnsFeatureFlags;
     private final Map<String, Integer> mServiceTypeToOffloadPriority;
+    private final ArraySet<String> mOffloadServiceTypeDenyList;
 
     /**
      * Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -160,6 +162,16 @@
         return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
     }
 
+    private boolean isInOffloadDenyList(@NonNull String serviceType) {
+        for (int i = 0; i < mOffloadServiceTypeDenyList.size(); ++i) {
+            final String denyListServiceType = mOffloadServiceTypeDenyList.valueAt(i);
+            if (DnsUtils.equalsIgnoreDnsCase(serviceType, denyListServiceType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
             new MdnsInterfaceAdvertiser.Callback() {
         @Override
@@ -173,19 +185,25 @@
             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
                     // TODO: Enable offload when the serviceInfo contains a custom host.
                     && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
-                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 String serviceType = registration.getServiceInfo().getServiceType();
+                if (isInOffloadDenyList(serviceType)) {
+                    mSharedLog.i("Offload denied for service type: " + serviceType);
+                } else {
+                    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);
 
-                byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
-                final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
-                        serviceId, registration, rawOffloadPacket);
-                existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
-                mCb.onOffloadStartOrUpdate(interfaceName,
-                        newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+                    byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
+                    final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper =
+                            createOffloadService(serviceId, registration, rawOffloadPacket);
+                    existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
+                    mCb.onOffloadStartOrUpdate(interfaceName,
+                            newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+                }
             }
 
             // Wait for all current interfaces to be done probing before notifying of success.
@@ -846,6 +864,8 @@
         final ConnectivityResources res = new ConnectivityResources(context);
         mServiceTypeToOffloadPriority = parseOffloadPriorityList(
                 res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+        mOffloadServiceTypeDenyList = new ArraySet<>(
+                res.get().getStringArray(R.array.config_nsdOffloadServicesDenyList));
     }
 
     private static Map<String, Integer> parseOffloadPriorityList(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index c833422..33bcb70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,24 +16,17 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.internal.annotations.VisibleForTesting.Visibility;
-
 import android.Manifest.permission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
-import androidx.annotation.GuardedBy;
-
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.DnsUtils;
-import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.SharedLog;
 
 import java.io.IOException;
@@ -41,7 +34,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.Executor;
 
 /**
  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -137,98 +129,6 @@
     }
 
     /**
-     * A utility class to generate a handler, optionally with a looper, and to run functions on the
-     * newly created handler.
-     */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static class DiscoveryExecutor implements Executor {
-        private final HandlerThread handlerThread;
-
-        @GuardedBy("pendingTasks")
-        @Nullable private Handler handler;
-        // Store pending tasks and associated delay time. Each Pair represents a pending task
-        // (first) and its delay time (second).
-        @GuardedBy("pendingTasks")
-        @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
-
-        DiscoveryExecutor(@Nullable Looper defaultLooper) {
-            if (defaultLooper != null) {
-                this.handlerThread = null;
-                synchronized (pendingTasks) {
-                    this.handler = new Handler(defaultLooper);
-                }
-            } else {
-                this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
-                    @Override
-                    protected void onLooperPrepared() {
-                        synchronized (pendingTasks) {
-                            handler = new Handler(getLooper());
-                            for (Pair<Runnable, Long> pendingTask : pendingTasks) {
-                                handler.postDelayed(pendingTask.first, pendingTask.second);
-                            }
-                            pendingTasks.clear();
-                        }
-                    }
-                };
-                this.handlerThread.start();
-            }
-        }
-
-        public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
-            if (this.handlerThread == null) {
-                // Callers are expected to already be running on the handler when a defaultLooper
-                // was provided
-                function.run();
-            } else {
-                execute(function);
-            }
-        }
-
-        @Override
-        public void execute(Runnable function) {
-            executeDelayed(function, 0L /* delayMillis */);
-        }
-
-        public void executeDelayed(Runnable function, long delayMillis) {
-            final Handler handler;
-            synchronized (pendingTasks) {
-                if (this.handler == null) {
-                    pendingTasks.add(Pair.create(function, delayMillis));
-                    return;
-                } else {
-                    handler = this.handler;
-                }
-            }
-            handler.postDelayed(function, delayMillis);
-        }
-
-        void shutDown() {
-            if (this.handlerThread != null) {
-                this.handlerThread.quitSafely();
-            }
-        }
-
-        void ensureRunningOnHandlerThread() {
-            synchronized (pendingTasks) {
-                HandlerUtils.ensureRunningOnHandlerThread(handler);
-            }
-        }
-
-        public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
-            final Handler handler;
-            synchronized (pendingTasks) {
-                if (this.handler == null) {
-                    Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
-                    return;
-                } else {
-                    handler = this.handler;
-                }
-            }
-            HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
-        }
-    }
-
-    /**
      * Do the cleanup of the MdnsDiscoveryManager
      */
     public void shutDown() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 2f3bdc5..1cf5e4d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -95,6 +95,11 @@
             "nsd_cached_services_retention_time";
     public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
 
+    /**
+     * A feature flag to control whether the accurate delay callback should be enabled.
+     */
+    public static final String NSD_ACCURATE_DELAY_CALLBACK = "nsd_accurate_delay_callback";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -128,6 +133,9 @@
     // Retention Time for cached services
     public final long mCachedServicesRetentionTime;
 
+    // Flag for accurate delay callback
+    public final boolean mIsAccurateDelayCallbackEnabled;
+
     // Flag to use shorter (16 characters + .local) hostnames
     public final boolean mIsShortHostnamesEnabled;
 
@@ -231,6 +239,14 @@
     }
 
     /**
+     * Indicates whether {@link #NSD_ACCURATE_DELAY_CALLBACK} is enabled, including for testing.
+     */
+    public boolean isAccurateDelayCallbackEnabled() {
+        return mIsAccurateDelayCallbackEnabled
+                || isForceEnabledForTest(NSD_ACCURATE_DELAY_CALLBACK);
+    }
+
+    /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -244,6 +260,7 @@
             boolean avoidAdvertisingEmptyTxtRecords,
             boolean isCachedServicesRemovalEnabled,
             long cachedServicesRetentionTime,
+            boolean isAccurateDelayCallbackEnabled,
             boolean isShortHostnamesEnabled,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
@@ -257,6 +274,7 @@
         mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
         mCachedServicesRetentionTime = cachedServicesRetentionTime;
+        mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
         mIsShortHostnamesEnabled = isShortHostnamesEnabled;
         mOverrideProvider = overrideProvider;
     }
@@ -281,6 +299,7 @@
         private boolean mAvoidAdvertisingEmptyTxtRecords;
         private boolean mIsCachedServicesRemovalEnabled;
         private long mCachedServicesRetentionTime;
+        private boolean mIsAccurateDelayCallbackEnabled;
         private boolean mIsShortHostnamesEnabled;
         private FlagOverrideProvider mOverrideProvider;
 
@@ -299,6 +318,7 @@
             mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mIsCachedServicesRemovalEnabled = false;
             mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+            mIsAccurateDelayCallbackEnabled = false;
             mIsShortHostnamesEnabled = true; // Default enabled.
             mOverrideProvider = null;
         }
@@ -426,6 +446,16 @@
         }
 
         /**
+         * Set whether the accurate delay callback is enabled.
+         *
+         * @see #NSD_ACCURATE_DELAY_CALLBACK
+         */
+        public Builder setIsAccurateDelayCallbackEnabled(boolean isAccurateDelayCallbackEnabled) {
+            mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+            return this;
+        }
+
+        /**
          * Set whether the short hostnames feature is enabled.
          *
          * @see #NSD_USE_SHORT_HOSTNAMES
@@ -450,6 +480,7 @@
                     mAvoidAdvertisingEmptyTxtRecords,
                     mIsCachedServicesRemovalEnabled,
                     mCachedServicesRetentionTime,
+                    mIsAccurateDelayCallbackEnabled,
                     mIsShortHostnamesEnabled,
                     mOverrideProvider);
         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 8c86fb8..56d4b9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -20,6 +20,7 @@
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
 
@@ -36,6 +37,7 @@
 
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DnsUtils;
+import com.android.net.module.util.RealtimeScheduler;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -94,6 +96,9 @@
     private final boolean removeServiceAfterTtlExpires =
             MdnsConfigs.removeServiceAfterTtlExpires();
     private final Clock clock;
+    // Use RealtimeScheduler for query scheduling, which allows for more accurate sending of
+    // queries.
+    @Nullable private final RealtimeScheduler realtimeScheduler;
 
     @Nullable private MdnsSearchOptions searchOptions;
 
@@ -139,8 +144,7 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_START_QUERYTASK: {
-                    final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
-                            (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
+                    final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
                     // QueryTask should be run immediately after being created (not be scheduled in
                     // advance). Because the result of "makeResponsesForResolve" depends on answers
                     // that were received before it is called, so to take into account all answers
@@ -174,7 +178,7 @@
                     final long now = clock.elapsedRealtime();
                     lastSentTime = now;
                     final long minRemainingTtl = getMinRemainingTtl(now);
-                    MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+                    final ScheduledQueryTaskArgs args =
                             mdnsQueryScheduler.scheduleNextRun(
                                     sentResult.taskArgs.config,
                                     minRemainingTtl,
@@ -189,10 +193,14 @@
                     sharedLog.log(String.format("Query sent with transactionId: %d. "
                                     + "Next run: sessionId: %d, in %d ms",
                             sentResult.transactionId, args.sessionId, timeToNextTaskMs));
-                    dependencies.sendMessageDelayed(
-                            handler,
-                            handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                            timeToNextTaskMs);
+                    if (realtimeScheduler != null) {
+                        setDelayedTask(args, timeToNextTaskMs);
+                    } else {
+                        dependencies.sendMessageDelayed(
+                                handler,
+                                handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                                timeToNextTaskMs);
+                    }
                     break;
                 }
                 default:
@@ -254,6 +262,14 @@
                 return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
             }
         }
+
+        /**
+         * @see RealtimeScheduler
+         */
+        @Nullable
+        public RealtimeScheduler createRealtimeScheduler(@NonNull Handler handler) {
+            return new RealtimeScheduler(handler);
+        }
     }
 
     /**
@@ -301,6 +317,8 @@
         this.mdnsQueryScheduler = new MdnsQueryScheduler();
         this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
         this.featureFlags = featureFlags;
+        this.realtimeScheduler = featureFlags.isAccurateDelayCallbackEnabled()
+                ? dependencies.createRealtimeScheduler(handler) : null;
     }
 
     /**
@@ -310,6 +328,9 @@
         removeScheduledTask();
         mdnsQueryScheduler.cancelScheduledRun();
         serviceCache.unregisterServiceExpiredCallback(cacheKey);
+        if (realtimeScheduler != null) {
+            realtimeScheduler.close();
+        }
     }
 
     private List<MdnsResponse> getExistingServices() {
@@ -317,6 +338,12 @@
                 ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
     }
 
+    private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
+        realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+        realtimeScheduler.sendDelayedMessage(
+                handler.obtainMessage(EVENT_START_QUERYTASK, args), timeToNextTaskMs);
+    }
+
     /**
      * Registers {@code listener} for receiving discovery event of mDNS service instances, and
      * starts
@@ -363,7 +390,7 @@
         }
         final long minRemainingTtl = getMinRemainingTtl(now);
         if (hadReply) {
-            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+            final ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.scheduleNextRun(
                             taskConfig,
                             minRemainingTtl,
@@ -377,10 +404,14 @@
             final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
             sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
                     args.sessionId, timeToNextTaskMs));
-            dependencies.sendMessageDelayed(
-                    handler,
-                    handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                    timeToNextTaskMs);
+            if (realtimeScheduler != null) {
+                setDelayedTask(args, timeToNextTaskMs);
+            } else {
+                dependencies.sendMessageDelayed(
+                        handler,
+                        handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                        timeToNextTaskMs);
+            }
         } else {
             final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
             final QueryTask queryTask = new QueryTask(
@@ -420,7 +451,11 @@
     }
 
     private void removeScheduledTask() {
-        dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+        if (realtimeScheduler != null) {
+            realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+        } else {
+            dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+        }
         sharedLog.log("Remove EVENT_START_QUERYTASK"
                 + ", current session: " + currentSessionId);
         ++currentSessionId;
@@ -506,10 +541,13 @@
                 }
             }
         }
-        if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
+        final boolean hasScheduledTask = realtimeScheduler != null
+                ? realtimeScheduler.hasDelayedMessage(EVENT_START_QUERYTASK)
+                : dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
+        if (hasScheduledTask) {
             final long now = clock.elapsedRealtime();
             final long minRemainingTtl = getMinRemainingTtl(now);
-            MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+            final ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
                             lastSentTime, currentSessionId + 1,
                             searchOptions.numOfQueriesBeforeBackoff());
@@ -518,10 +556,14 @@
                 final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
                 sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
                         args.sessionId, timeToNextTaskMs));
-                dependencies.sendMessageDelayed(
-                        handler,
-                        handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                        timeToNextTaskMs);
+                if (realtimeScheduler != null) {
+                    setDelayedTask(args, timeToNextTaskMs);
+                } else {
+                    dependencies.sendMessageDelayed(
+                            handler,
+                            handler.obtainMessage(EVENT_START_QUERYTASK, args),
+                            timeToNextTaskMs);
+                }
             }
         }
     }
@@ -686,10 +728,10 @@
     private static class QuerySentArguments {
         private final int transactionId;
         private final List<String> subTypes = new ArrayList<>();
-        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+        private final ScheduledQueryTaskArgs taskArgs;
 
         QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
-                @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
+                @NonNull ScheduledQueryTaskArgs taskArgs) {
             this.transactionId = transactionId;
             this.subTypes.addAll(subTypes);
             this.taskArgs = taskArgs;
@@ -698,14 +740,14 @@
 
     // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
     private class QueryTask implements Runnable {
-        private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+        private final ScheduledQueryTaskArgs taskArgs;
         private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
         private final List<String> subtypes = new ArrayList<>();
         private final boolean sendDiscoveryQueries;
         private final List<MdnsResponse> existingServices = new ArrayList<>();
         private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
         private final SocketKey socketKey;
-        QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
+        QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
                 @NonNull Collection<MdnsResponse> servicesToResolve,
                 @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
                 @NonNull Collection<MdnsResponse> existingServices,
@@ -771,7 +813,7 @@
         return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
     }
 
-    private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
+    private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args,
             long now) {
         return Math.max(args.timeToRun - now, 0);
     }
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 2d3647a..1b0f29d 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -135,6 +135,15 @@
     <string-array translatable="false" name="config_nsdOffloadServicesPriority">
     </string-array>
 
+    <!-- An array of service types that shouldn't be offloaded via NsdManager#registerOffloadEngine.
+         Format is [service type], for example: "_testservice._tcp"
+         Due to limited RAM in hardware offload, we prioritize user-impacting services.
+         _googlezone._tcp, an internal Googlecast service, was therefore blocked.
+    -->
+    <string-array name="config_nsdOffloadServicesDenyList" translatable="false">
+        <item>_googlezone._tcp</item>
+    </string-array>
+
     <!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
          notification that can be dismissed. -->
     <bool name="config_ongoingSignInNotification">false</bool>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 7ac86aa..5c0ba78 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
             <item type="integer" name="config_networkWakeupPacketMask"/>
             <item type="integer" name="config_networkNotifySwitchType"/>
             <item type="array" name="config_networkNotifySwitches"/>
+            <item type="array" name="config_nsdOffloadServicesDenyList"/>
             <item type="array" name="config_nsdOffloadServicesPriority"/>
             <item type="bool" name="config_ongoingSignInNotification"/>
             <item type="bool" name="config_autoCancelNetworkNotifications"/>
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 0eab6e7..8034e57 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -438,7 +438,11 @@
     srcs: [
         "device/com/android/net/module/util/FdEventsReader.java",
         "device/com/android/net/module/util/HandlerUtils.java",
+        "device/com/android/net/module/util/JniUtil.java",
+        "device/com/android/net/module/util/RealtimeScheduler.java",
         "device/com/android/net/module/util/SharedLog.java",
+        "device/com/android/net/module/util/ServiceConnectivityJni.java",
+        "device/com/android/net/module/util/TimerFdUtils.java",
         "framework/com/android/net/module/util/ByteUtils.java",
         "framework/com/android/net/module/util/CollectionUtils.java",
         "framework/com/android/net/module/util/DnsUtils.java",
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 4227194..d79f52e 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -23,6 +23,7 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.nio.ByteBuffer;
@@ -132,17 +133,35 @@
         }
     }
 
+    public static class Dependencies {
+        /**
+         * Call {@link Os#access}
+         */
+        public boolean access(String path, int mode) throws ErrnoException {
+            return Os.access(path, mode);
+        }
+    }
+
     /**
      * Dump the BpfMap status
      */
     public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
             PrintWriter pw, String mapName, String path) {
+        dumpMapStatus(map, pw, mapName, path, new Dependencies());
+    }
+
+    /**
+     * Dump the BpfMap status. Only test should use this method directly.
+     */
+    @VisibleForTesting
+    public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
+            PrintWriter pw, String mapName, String path, Dependencies deps) {
         if (map != null) {
             pw.println(mapName + ": OK");
             return;
         }
         try {
-            Os.access(path, R_OK);
+            deps.access(path, R_OK);
             pw.println(mapName + ": NULL(map is pinned to " + path + ")");
         } catch (ErrnoException e) {
             pw.println(mapName + ": NULL(map is not pinned to " + path + ": "
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
index c8fdf72..2d95223 100644
--- a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -227,6 +227,13 @@
         return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
     }
 
+    private static boolean isMessageTask(Task task, int what) {
+        if (task instanceof MessageTask && ((MessageTask) task).mMessage.what == what) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Remove a scheduled message.
      *
@@ -234,8 +241,24 @@
      */
     public void removeDelayedMessage(int what) {
         ensureRunningOnCorrectThread();
-        mTaskQueue.removeIf(task -> task instanceof MessageTask
-                && ((MessageTask) task).mMessage.what == what);
+        mTaskQueue.removeIf(task -> isMessageTask(task, what));
+    }
+
+    /**
+     * Check if there is a scheduled message.
+     *
+     * @param what the message to be checked
+     * @return true if there is a target message, false otherwise.
+     */
+    public boolean hasDelayedMessage(int what) {
+        ensureRunningOnCorrectThread();
+
+        for (Task task : mTaskQueue) {
+            if (isMessageTask(task, what)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 419b338..348df3b 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -103,8 +103,12 @@
       self, mock_adb_shell: MagicMock
   ) -> None:
     mock_adb_shell.return_value = """
+45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+ link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
  link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+ link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff
 """
     mac_address = get_hardware_address(self.mock_ad, "wlan0")
     asserts.assert_equal(mac_address, "72:05:77:82:21:E0")
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
index a66dacd..673d9e6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -17,18 +17,13 @@
 package com.android.net.module.util;
 
 import static android.system.OsConstants.EPERM;
-import static android.system.OsConstants.R_OK;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.Nullable;
 import android.system.ErrnoException;
-import android.system.Os;
 import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
@@ -38,7 +33,6 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.MockitoSession;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -129,9 +123,14 @@
         assertTrue(dump.contains("key=789, val=123"));
     }
 
-    private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map) {
+    private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map,
+            @Nullable final BpfDump.Dependencies deps) {
         final StringWriter sw = new StringWriter();
-        BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+        if (deps == null) {
+            BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+        } else {
+            BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath", deps);
+        }
         return sw.toString();
     }
 
@@ -139,25 +138,34 @@
     public void testGetMapStatus() {
         final IBpfMap<Struct.S32, Struct.S32> map =
                 new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
-        assertEquals("mapName: OK\n", getDumpMapStatus(map));
+        assertEquals("mapName: OK\n", getDumpMapStatus(map, null /* deps */));
     }
 
     @Test
-    public void testGetMapStatusNull() {
-        final MockitoSession session = mockitoSession()
-                .spyStatic(Os.class)
-                .startMocking();
-        try {
-            // Os.access succeeds
-            doReturn(true).when(() -> Os.access("mapPath", R_OK));
-            assertEquals("mapName: NULL(map is pinned to mapPath)\n", getDumpMapStatus(null));
+    public void testGetMapStatusNull_accessSucceed() {
+        // Os.access succeeds
+        assertEquals("mapName: NULL(map is pinned to mapPath)\n",
+                getDumpMapStatus(null /* map */,
+                        new BpfDump.Dependencies() {
+                            @Override
+                            public boolean access(String path, int mode) {
+                                return true;
+                            }
+                        })
+        );
+    }
 
-            // Os.access throws EPERM
-            doThrow(new ErrnoException("", EPERM)).when(() -> Os.access("mapPath", R_OK));
-            assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
-                    getDumpMapStatus(null));
-        } finally {
-            session.finishMocking();
-        }
+    @Test
+    public void testGetMapStatusNull_accessThrow() {
+        // Os.access throws EPERM
+        assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
+                getDumpMapStatus(null /* map */,
+                        new BpfDump.Dependencies(){
+                            @Override
+                            public boolean access(String path, int mode) throws ErrnoException {
+                                throw new ErrnoException("", EPERM);
+                            }
+                        })
+        );
     }
 }
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 55ac860..49ffad6 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -162,14 +162,18 @@
   """
 
   # Run the "ip link" command and get its output.
-  ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
+  ip_link_output = adb_utils.adb_shell(ad, f"ip link")
 
   # Regular expression to extract the MAC address.
   # Parse hardware address from ip link output like below:
+  # 45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+  #    link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
   # 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
   #    link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
-  pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
-  match = re.search(pattern, ip_link_output)
+  # 47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+  #    link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff"
+  pattern = rf"{iface_name}:.*?link/ether (([0-9a-fA-F]{{2}}:){{5}}[0-9a-fA-F]{{2}})"
+  match = re.search(pattern, ip_link_output, re.DOTALL)
 
   if match:
     return match.group(1).upper()  # Extract the MAC address string.
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
new file mode 100644
index 0000000..51539a0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.connectivity.mdns
+
+import android.os.Build
+import android.os.HandlerThread
+import android.testing.TestableLooper
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val DEFAULT_TIMEOUT = 2000L
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class DiscoveryExecutorTest {
+    private val thread = HandlerThread(DiscoveryExecutorTest::class.simpleName).apply { start() }
+
+    @After
+    fun tearDown() {
+        thread.quitSafely()
+        thread.join()
+    }
+
+    @Test
+    fun testCheckAndRunOnHandlerThread() {
+        val testableLooper = TestableLooper(thread.looper)
+        val executor = DiscoveryExecutor(testableLooper.looper)
+        try {
+            val future = CompletableFuture<Boolean>()
+            executor.checkAndRunOnHandlerThread { future.complete(true) }
+            testableLooper.processAllMessages()
+            assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+        } finally {
+            testableLooper.destroy()
+        }
+
+        // Create a DiscoveryExecutor with the null defaultLooper and verify the task can execute
+        // normally.
+        val executor2 = DiscoveryExecutor(null /* defaultLooper */)
+        val future2 = CompletableFuture<Boolean>()
+        executor2.checkAndRunOnHandlerThread { future2.complete(true) }
+        assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+        executor2.shutDown()
+    }
+
+    @Test
+    fun testExecute() {
+        val testableLooper = TestableLooper(thread.looper)
+        val executor = DiscoveryExecutor(testableLooper.looper)
+        try {
+            val future = CompletableFuture<Boolean>()
+            executor.execute { future.complete(true) }
+            assertFalse(future.isDone)
+            testableLooper.processAllMessages()
+            assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+        } finally {
+            testableLooper.destroy()
+        }
+    }
+
+    @Test
+    fun testExecuteDelayed() {
+        val testableLooper = TestableLooper(thread.looper)
+        val executor = DiscoveryExecutor(testableLooper.looper)
+        try {
+            // Verify the executeDelayed method
+            val future = CompletableFuture<Boolean>()
+            // Schedule a task with 999 ms delay
+            executor.executeDelayed({ future.complete(true) }, 999L)
+            testableLooper.processAllMessages()
+            assertFalse(future.isDone)
+
+            // 500 ms have elapsed but do not exceed the target time (999 ms)
+            // The function should not be executed.
+            testableLooper.moveTimeForward(500L)
+            testableLooper.processAllMessages()
+            assertFalse(future.isDone)
+
+            // 500 ms have elapsed again and have exceeded the target time (999 ms).
+            // The function should be executed.
+            testableLooper.moveTimeForward(500L)
+            testableLooper.processAllMessages()
+            assertTrue(future.get(500L, TimeUnit.MILLISECONDS))
+        } finally {
+            testableLooper.destroy()
+        }
+    }
+}
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 e6e6ecc..087617a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -99,6 +99,13 @@
     network = TEST_NETWORK_1
 }
 
+private val GOOGLEZONE_SERVICE = NsdServiceInfo("TestServiceName", "_GOOglezone._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = TEST_NETWORK_1
+}
+
 private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     subtypes = setOf(TEST_SUBTYPE)
     port = 12345
@@ -183,6 +190,10 @@
     "5:_otherprioritytest._tcp"
 )
 
+private val SERVICES_DENY_LIST = arrayOf(
+    "_googlezone._tcp",
+)
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsAdvertiserTest {
@@ -247,6 +258,9 @@
         doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
             R.array.config_nsdOffloadServicesPriority
         )
+        doReturn(SERVICES_DENY_LIST).`when`(resources).getStringArray(
+            R.array.config_nsdOffloadServicesDenyList
+        )
         ConnectivityResources.setResourcesContextForTest(context)
     }
 
@@ -524,6 +538,44 @@
     }
 
     @Test
+    fun testAddService_NoOffloadForServiceTypeInDenyList() {
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync {
+            advertiser.addOrUpdateService(
+                SERVICE_ID_1,
+                GOOGLEZONE_SERVICE,
+                DEFAULT_ADVERTISING_OPTION,
+                TEST_CLIENT_UID_1
+            )
+        }
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor1.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
+        )
+
+        doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+        postSync {
+            intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+        }
+
+        verify(cb, never()).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), any())
+    }
+
+    @Test
     fun testAddService_NoSubtypeForGoogleCastOffload() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 71a3274..758b822 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,8 +19,6 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
@@ -35,12 +33,10 @@
 import android.net.Network;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.Pair;
 
 import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
 import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -60,9 +56,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link MdnsDiscoveryManager}. */
 @DevSdkIgnoreRunner.MonitorThreadLeak
@@ -435,45 +429,6 @@
     }
 
     @Test
-    public void testDiscoveryExecutor() throws Exception {
-        final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
-        final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
-        try {
-            // Verify the checkAndRunOnHandlerThread method
-            final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
-            executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
-            assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
-            // Verify the execute method
-            final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
-            executor.execute(()-> future2.complete(true));
-            testableLooper.processAllMessages();
-            assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
-            // Verify the executeDelayed method
-            final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
-            // Schedule a task with 999 ms delay
-            executor.executeDelayed(()-> future3.complete(true), 999L);
-            testableLooper.processAllMessages();
-            assertFalse(future3.isDone());
-
-            // 500 ms have elapsed but do not exceed the target time (999 ms)
-            // The function should not be executed.
-            testableLooper.moveTimeForward(500L);
-            testableLooper.processAllMessages();
-            assertFalse(future3.isDone());
-
-            // 500 ms have elapsed again and have exceeded the target time (999 ms).
-            // The function should be executed.
-            testableLooper.moveTimeForward(500L);
-            testableLooper.processAllMessages();
-            assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
-        } finally {
-            testableLooper.destroy();
-        }
-    }
-
-    @Test
     public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
         final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
                 .setIsCachedServicesRemovalEnabled(true)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 67f9d9c..dad03e0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -59,6 +59,7 @@
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.RealtimeScheduler;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -127,6 +128,8 @@
     private SharedLog mockSharedLog;
     @Mock
     private MdnsServiceTypeClient.Dependencies mockDeps;
+    @Mock
+    private RealtimeScheduler mockRealtimeScheduler;
     @Captor
     private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
 
@@ -145,6 +148,7 @@
     private Message delayMessage = null;
     private Handler realHandler = null;
     private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
+    private Message message = null;
 
     @Before
     @SuppressWarnings("DoNotMock")
@@ -244,10 +248,21 @@
             return true;
         }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
 
-        client = makeMdnsServiceTypeClient();
+        doAnswer(inv -> {
+            realHandler = (Handler) inv.getArguments()[0];
+            return mockRealtimeScheduler;
+        }).when(mockDeps).createRealtimeScheduler(any(Handler.class));
+
+        doAnswer(inv -> {
+            message = (Message) inv.getArguments()[0];
+            latestDelayMs = (long) inv.getArguments()[1];
+            return null;
+        }).when(mockRealtimeScheduler).sendDelayedMessage(any(), anyLong());
+
+        client = makeMdnsServiceTypeClient(featureFlags);
     }
 
-    private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient(MdnsFeatureFlags featureFlags) {
         return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
                 mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
                 serviceCache, featureFlags);
@@ -1926,9 +1941,7 @@
 
     @Test
     public void testSendQueryWithKnownAnswers() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache,
+        client = makeMdnsServiceTypeClient(
                 MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
 
         doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -1990,9 +2003,7 @@
 
     @Test
     public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
-        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
-                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache,
+        client = makeMdnsServiceTypeClient(
                 MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
 
         doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2114,6 +2125,73 @@
         assertEquals(9680L, latestDelayMs);
     }
 
+    @Test
+    public void sendQueries_AccurateDelayCallback() {
+        client = makeMdnsServiceTypeClient(
+                MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+        final int numOfQueriesBeforeBackoff = 2;
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .addSubtype(SUBTYPE)
+                .setQueryMode(AGGRESSIVE_QUERY_MODE)
+                .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+        // Verify that the first query has been sent.
+        verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 1 /* scheduledCount */,
+                1 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+        // Verify that the second query has been sent
+        verifyAndSendQuery(1 /* index */, 0 /* timeInMs */, false /* expectsUnicastResponse */,
+                true /* multipleSocketDiscovery */, 2 /* scheduledCount */,
+                2 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+        // Verify that the third query has been sent
+        verifyAndSendQuery(2 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                3 /* scheduledCount */, 3 /* sendMessageCount */,
+                true /* useAccurateDelayCallback */);
+
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockRealtimeScheduler).hasDelayedMessage(EVENT_START_QUERYTASK);
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        // Verify that the message removal occurred.
+        verify(mockRealtimeScheduler, times(6)).removeDelayedMessage(EVENT_START_QUERYTASK);
+        assertNotNull(message);
+        verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+                true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+                5 /* scheduledCount */, 4 /* sendMessageCount */,
+                true /* useAccurateDelayCallback */);
+
+        // Stop sending packets.
+        stopSendAndReceive(mockListenerOne);
+        verify(mockRealtimeScheduler, times(8)).removeDelayedMessage(EVENT_START_QUERYTASK);
+    }
+
+    @Test
+    public void testTimerFdCloseProperly() {
+        client = makeMdnsServiceTypeClient(
+                MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+        // Start query
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.newBuilder().build());
+        verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+        // Stop query and verify the close() method has been called.
+        stopSendAndReceive(mockListenerOne);
+        verify(mockRealtimeScheduler, times(2)).removeDelayedMessage(EVENT_START_QUERYTASK);
+        verify(mockRealtimeScheduler).close();
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
@@ -2127,9 +2205,22 @@
 
     private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
             boolean multipleSocketDiscovery, int scheduledCount) {
-        // Dispatch the message
-        if (delayMessage != null && realHandler != null) {
-            dispatchMessage();
+        verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
+                multipleSocketDiscovery, scheduledCount, index + 1 /* sendMessageCount */,
+                false /* useAccurateDelayCallback */);
+    }
+
+    private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
+            boolean multipleSocketDiscovery, int scheduledCount, int sendMessageCount,
+            boolean useAccurateDelayCallback) {
+        if (useAccurateDelayCallback && message != null && realHandler != null) {
+            runOnHandler(() -> realHandler.dispatchMessage(message));
+            message = null;
+        } else {
+            // Dispatch the message
+            if (delayMessage != null && realHandler != null) {
+                dispatchMessage();
+            }
         }
         assertEquals(timeInMs, latestDelayMs);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -2152,11 +2243,16 @@
                         eq(socketKey), eq(false));
             }
         }
-        verify(mockDeps, times(index + 1))
+        verify(mockDeps, times(sendMessageCount))
                 .sendMessage(any(Handler.class), any(Message.class));
         // Verify the task has been scheduled.
-        verify(mockDeps, times(scheduledCount))
-                .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+        if (useAccurateDelayCallback) {
+            verify(mockRealtimeScheduler, times(scheduledCount))
+                    .sendDelayedMessage(any(), anyLong());
+        } else {
+            verify(mockDeps, times(scheduledCount))
+                    .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+        }
     }
 
     private static String[] getTestServiceName(String instanceName) {