Add DiscoveryExecutor#executeDelayed

This method can execute the task after the specified amount of
time elapses.

Bug: 355421878
Test: atest FrameworksNetTests NsdManagerTest
Change-Id: I9d749393bd5435cdba5072d48242cd0d17332b67
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 7fa605a..a74bdf7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,6 +16,8 @@
 
 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;
@@ -134,13 +136,20 @@
         this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
     }
 
-    private static class DiscoveryExecutor implements Executor {
+    /**
+     * 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<Runnable> pendingTasks = new ArrayList<>();
+        @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
 
         DiscoveryExecutor(@Nullable Looper defaultLooper) {
             if (defaultLooper != null) {
@@ -154,8 +163,8 @@
                     protected void onLooperPrepared() {
                         synchronized (pendingTasks) {
                             handler = new Handler(getLooper());
-                            for (Runnable pendingTask : pendingTasks) {
-                                handler.post(pendingTask);
+                            for (Pair<Runnable, Long> pendingTask : pendingTasks) {
+                                handler.postDelayed(pendingTask.first, pendingTask.second);
                             }
                             pendingTasks.clear();
                         }
@@ -177,16 +186,20 @@
 
         @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(function);
+                    pendingTasks.add(Pair.create(function, delayMillis));
                     return;
                 } else {
                     handler = this.handler;
                 }
             }
-            handler.post(function);
+            handler.postDelayed(function, delayMillis);
         }
 
         void shutDown() {
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 b5c0132..ec47618 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,6 +19,8 @@
 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;
@@ -32,10 +34,12 @@
 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;
@@ -55,7 +59,9 @@
 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
@@ -390,6 +396,48 @@
         verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
     }
 
+    @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.isDone());
+            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.isDone());
+            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.isDone());
+            assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
+        } finally {
+            testableLooper.destroy();
+        }
+    }
+
     private MdnsPacket createMdnsPacket(String serviceType) {
         final String[] type = TextUtils.split(serviceType, "\\.");
         final ArrayList<String> name = new ArrayList<>(type.length + 1);