Snap for 13182071 from 5acb3ae6768ed54caab71c0403bf7cec783ea3a9 to 25Q2-release

Change-Id: I2510dc93c591f8b2f83273fdb271679b27711b94
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 609d759..d0ba431 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -852,16 +852,13 @@
         }
     }
 
-    private void removeRoutesFromNetworkAndLinkProperties(int netId,
-            @NonNull final List<RouteInfo> toBeRemoved) {
+    private void removeRoutesFromNetwork(int netId, @NonNull final List<RouteInfo> toBeRemoved) {
         final int removalFailures = NetdUtils.removeRoutesFromNetwork(
                 mNetd, netId, toBeRemoved);
         if (removalFailures > 0) {
             mLog.e("Failed to remove " + removalFailures
                     + " IPv6 routes from network " + netId + ".");
         }
-
-        for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
     }
 
     private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
@@ -888,7 +885,7 @@
         }
     }
 
-    private void addRoutesToNetworkAndLinkProperties(int netId,
+    private void addRoutesToNetwork(int netId,
             @NonNull final List<RouteInfo> toBeAdded) {
         // It's safe to call addInterfaceToNetwork() even if
         // the interface is already in the network.
@@ -901,16 +898,16 @@
             mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
             return;
         }
-
-        for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
     }
 
     private void configureLocalIPv6Routes(
             ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
         // [1] Remove the routes that are deprecated.
         if (!deprecatedPrefixes.isEmpty()) {
-            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
-                    getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+            final List<RouteInfo> routesToBeRemoved =
+                    getLocalRoutesFor(mIfaceName, deprecatedPrefixes);
+            removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
         }
 
         // [2] Add only the routes that have not previously been added.
@@ -921,8 +918,10 @@
             }
 
             if (!addedPrefixes.isEmpty()) {
-                addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
-                        getLocalRoutesFor(mIfaceName, addedPrefixes));
+                final List<RouteInfo> routesToBeAdded =
+                        getLocalRoutesFor(mIfaceName, addedPrefixes);
+                addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+                for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
             }
         }
     }
@@ -1126,8 +1125,17 @@
             }
 
             try {
-                NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
-                        asIpPrefix(mIpv4Address));
+                // Enable IPv6, disable accepting RA, etc. See TetherController::tetherInterface()
+                // for more detail.
+                mNetd.tetherInterfaceAdd(mIfaceName);
+                NetdUtils.networkAddInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+                        20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+                // Activate a route to dest and IPv6 link local.
+                NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+                        new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, RTN_UNICAST));
+                NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+                        new RouteInfo(new IpPrefix("fe80::/64"), null, mIfaceName,
+                                RTN_UNICAST));
             } catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
                 mLog.e("Error Tethering", e);
                 mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1148,8 +1156,13 @@
             // all in sequence.
             stopIPv6();
 
+            // Reset interface for tethering.
             try {
-                NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
+                try {
+                    mNetd.tetherInterfaceRemove(mIfaceName);
+                } finally {
+                    mNetd.networkRemoveInterface(LOCAL_NET_ID, mIfaceName);
+                }
             } catch (RemoteException | ServiceSpecificException e) {
                 mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
                 mLog.e("Failed to untether interface: " + e);
@@ -1227,13 +1240,16 @@
             }
 
             // Remove deprecated routes from downstream network.
-            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
-                    List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
+            final List<RouteInfo> routesToBeRemoved =
+                    List.of(getDirectConnectedRoute(deprecatedLinkAddress));
+            removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
             mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
 
             // Add new routes to downstream network.
-            addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
-                    List.of(getDirectConnectedRoute(mIpv4Address)));
+            final List<RouteInfo> routesToBeAdded = List.of(getDirectConnectedRoute(mIpv4Address));
+            addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+            for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
             mLinkProperties.addLinkAddress(mIpv4Address);
 
             // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
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/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/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/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 8b2fe58..4af516d 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -148,25 +148,6 @@
         netd.tetherStartWithConfiguration(config);
     }
 
-    /** Setup interface for tethering. */
-    public static void tetherInterface(final INetd netd, int netId, final String iface,
-            final IpPrefix dest) throws RemoteException, ServiceSpecificException {
-        tetherInterface(netd, netId, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
-    }
-
-    /** Setup interface with configurable retries for tethering. */
-    public static void tetherInterface(final INetd netd, int netId, final String iface,
-            final IpPrefix dest, int maxAttempts, int pollingIntervalMs)
-            throws RemoteException, ServiceSpecificException {
-        netd.tetherInterfaceAdd(iface);
-        networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
-        // Activate a route to dest and IPv6 link local.
-        modifyRoute(netd, ModifyOperation.ADD, netId,
-                new RouteInfo(dest, null, iface, RTN_UNICAST));
-        modifyRoute(netd, ModifyOperation.ADD, netId,
-                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
-    }
-
     /**
      * Retry Netd#networkAddInterface for EBUSY error code.
      * If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
@@ -174,7 +155,7 @@
      * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
      * See b/158269544 for detail.
      */
-    private static void networkAddInterface(final INetd netd, int netId, final String iface,
+    public static void networkAddInterface(final INetd netd, int netId, final String iface,
             int maxAttempts, int pollingIntervalMs)
             throws ServiceSpecificException, RemoteException {
         for (int i = 1; i <= maxAttempts; i++) {
@@ -193,16 +174,6 @@
         }
     }
 
-    /** Reset interface for tethering. */
-    public static void untetherInterface(final INetd netd, int netId, String iface)
-            throws RemoteException, ServiceSpecificException {
-        try {
-            netd.tetherInterfaceRemove(iface);
-        } finally {
-            netd.networkRemoveInterface(netId, iface);
-        }
-    }
-
     /** Add |routes| to the given network. */
     public static void addRoutesToNetwork(final INetd netd, int netId, final String iface,
             final List<RouteInfo> routes) {
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index c2fbb56..0241d0a 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -24,13 +24,9 @@
 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.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -39,7 +35,6 @@
 
 import android.net.INetd;
 import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
@@ -61,7 +56,6 @@
     @Mock private INetd mNetd;
 
     private static final String IFACE = "TEST_IFACE";
-    private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
     private static final int TEST_NET_ID = 123;
 
     @Before
@@ -150,20 +144,21 @@
     }
 
     @Test
-    public void testTetherInterfaceSuccessful() throws Exception {
+    public void testNetworkAddInterfaceSuccessful() throws Exception {
         // Expect #networkAddInterface successful at first tries.
-        verifyTetherInterfaceSucceeds(1);
+        verifyNetworkAddInterfaceSucceeds(1);
 
         // Expect #networkAddInterface successful after 10 tries.
-        verifyTetherInterfaceSucceeds(10);
+        verifyNetworkAddInterfaceSucceeds(10);
     }
 
-    private void runTetherInterfaceWithServiceSpecificException(int expectedTries,
+    private void runNetworkAddInterfaceWithServiceSpecificException(int expectedTries,
             int expectedCode) throws Exception {
         setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+                    20 /* maxAttempts */, 0 /* pollingIntervalMs */);
             fail("Expect throw ServiceSpecificException");
         } catch (ServiceSpecificException e) {
             assertEquals(e.errorCode, expectedCode);
@@ -173,11 +168,12 @@
         reset(mNetd);
     }
 
-    private void runTetherInterfaceWithRemoteException(int expectedTries) throws Exception {
+    private void runNetworkAddInterfaceWithRemoteException(int expectedTries) throws Exception {
         setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+                    20 /* maxAttempts */, 0 /* pollingIntervalMs */);
             fail("Expect throw RemoteException");
         } catch (RemoteException e) { }
 
@@ -186,41 +182,37 @@
     }
 
     private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
-        verify(mNetd).tetherInterfaceAdd(IFACE);
         verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
-        verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
-
         verifyNoMoreInteractions(mNetd);
     }
 
-    private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+    private void verifyNetworkAddInterfaceSucceeds(int expectedTries) throws Exception {
         setNetworkAddInterfaceOutcome(null, expectedTries);
 
-        NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
-        verify(mNetd).tetherInterfaceAdd(IFACE);
+        NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+                20 /* maxAttempts */, 0 /* pollingIntervalMs */);
         verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
-        verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
         verifyNoMoreInteractions(mNetd);
         reset(mNetd);
     }
 
     @Test
-    public void testTetherInterfaceFailOnNetworkAddInterface() throws Exception {
+    public void testFailOnNetworkAddInterface() throws Exception {
         // Test throwing ServiceSpecificException with EBUSY failure.
-        runTetherInterfaceWithServiceSpecificException(20, EBUSY);
+        runNetworkAddInterfaceWithServiceSpecificException(20, EBUSY);
 
         // Test throwing ServiceSpecificException with unexpectedError.
         final int unexpectedError = 999;
-        runTetherInterfaceWithServiceSpecificException(1, unexpectedError);
+        runNetworkAddInterfaceWithServiceSpecificException(1, unexpectedError);
 
         // Test throwing ServiceSpecificException with unexpectedError after 7 tries.
-        runTetherInterfaceWithServiceSpecificException(7, unexpectedError);
+        runNetworkAddInterfaceWithServiceSpecificException(7, unexpectedError);
 
         // Test throwing RemoteException.
-        runTetherInterfaceWithRemoteException(1);
+        runNetworkAddInterfaceWithRemoteException(1);
 
         // Test throwing RemoteException after 3 tries.
-        runTetherInterfaceWithRemoteException(3);
+        runNetworkAddInterfaceWithRemoteException(3);
     }
 
     @Test
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/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) {