[Feature Sync] Remove expired service

The service will be removed after TTL. The client can enable this
feature by setting MdnsSearchOptions#Builder#\
setRemoveExpiredService(true).

Bug: 254155029
Test: atest FrameworksNetTests
Change-Id: I7feac748eb2f239316492e95626433b136e63392
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
index 922037b..35a685d 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -89,4 +89,12 @@
     public static boolean preferIpv6() {
         return false;
     }
+
+    public static boolean removeServiceAfterTtlExpires() {
+        return false;
+    }
+
+    public static boolean allowSearchOptionsToRemoveExpiredService() {
+        return false;
+    }
 }
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index 6e90d2c..195bc8e 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -43,7 +43,7 @@
                 @Override
                 public MdnsSearchOptions createFromParcel(Parcel source) {
                     return new MdnsSearchOptions(source.createStringArrayList(),
-                            source.readBoolean());
+                            source.readBoolean(), source.readBoolean());
                 }
 
                 @Override
@@ -55,14 +55,16 @@
     private final List<String> subtypes;
 
     private final boolean isPassiveMode;
+    private final boolean removeExpiredService;
 
     /** Parcelable constructs for a {@link MdnsServiceInfo}. */
-    MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
+    MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode, boolean removeExpiredService) {
         this.subtypes = new ArrayList<>();
         if (subtypes != null) {
             this.subtypes.addAll(subtypes);
         }
         this.isPassiveMode = isPassiveMode;
+        this.removeExpiredService = removeExpiredService;
     }
 
     /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
@@ -91,6 +93,11 @@
         return isPassiveMode;
     }
 
+    /** Returns {@code true} if service will be removed after its TTL expires. */
+    public boolean removeExpiredService() {
+        return removeExpiredService;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -100,12 +107,14 @@
     public void writeToParcel(Parcel out, int flags) {
         out.writeStringList(subtypes);
         out.writeBoolean(isPassiveMode);
+        out.writeBoolean(removeExpiredService);
     }
 
     /** A builder to create {@link MdnsSearchOptions}. */
     public static final class Builder {
         private final Set<String> subtypes;
         private boolean isPassiveMode = true;
+        private boolean removeExpiredService;
 
         private Builder() {
             subtypes = new ArraySet<>();
@@ -136,8 +145,7 @@
 
         /**
          * Sets if the passive mode scan should be used. The passive mode scans less frequently in
-         * order
-         * to conserve battery and produce less network traffic.
+         * order to conserve battery and produce less network traffic.
          *
          * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
          *                      false}, active mode will be used.
@@ -147,9 +155,20 @@
             return this;
         }
 
+        /**
+         * Sets if the service should be removed after TTL.
+         *
+         * @param removeExpiredService If set to {@code true}, the service will be removed after TTL
+         */
+        public Builder setRemoveExpiredService(boolean removeExpiredService) {
+            this.removeExpiredService = removeExpiredService;
+            return this;
+        }
+
         /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
         public MdnsSearchOptions build() {
-            return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
+            return new MdnsSearchOptions(
+                    new ArrayList<>(subtypes), isPassiveMode, removeExpiredService);
         }
     }
 }
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index e335de9..4fbc809 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,7 +16,11 @@
 
 package com.android.server.connectivity.mdns;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Pair;
@@ -28,12 +32,12 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Instance of this class sends and receives mDNS packets of a given service type and invoke
@@ -53,6 +57,12 @@
     private final Object lock = new Object();
     private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
     private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+    private final boolean removeServiceAfterTtlExpires =
+            MdnsConfigs.removeServiceAfterTtlExpires();
+    private final boolean allowSearchOptionsToRemoveExpiredService =
+            MdnsConfigs.allowSearchOptionsToRemoveExpiredService();
+
+    @Nullable private MdnsSearchOptions searchOptions;
 
     // The session ID increases when startSendAndReceive() is called where we schedule a
     // QueryTask for
@@ -115,6 +125,7 @@
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
         synchronized (lock) {
+            this.searchOptions = searchOptions;
             if (!listeners.contains(listener)) {
                 listeners.add(listener);
                 for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
@@ -164,10 +175,23 @@
     }
 
     public synchronized void processResponse(@NonNull MdnsResponse response) {
-        if (response.isGoodbye()) {
-            onGoodbyeReceived(response.getServiceInstanceName());
+        if (shouldRemoveServiceAfterTtlExpires()) {
+            // Because {@link QueryTask} and {@link processResponse} are running in different
+            // threads. We need to synchronize {@link lock} to protect
+            // {@link instanceNameToResponse} won’t be modified at the same time.
+            synchronized (lock) {
+                if (response.isGoodbye()) {
+                    onGoodbyeReceived(response.getServiceInstanceName());
+                } else {
+                    onResponseReceived(response);
+                }
+            }
         } else {
-            onResponseReceived(response);
+            if (response.isGoodbye()) {
+                onGoodbyeReceived(response.getServiceInstanceName());
+            } else {
+                onResponseReceived(response);
+            }
         }
     }
 
@@ -212,6 +236,15 @@
         }
     }
 
+    private boolean shouldRemoveServiceAfterTtlExpires() {
+        if (removeServiceAfterTtlExpires) {
+            return true;
+        }
+        return allowSearchOptionsToRemoveExpiredService
+                && searchOptions != null
+                && searchOptions.removeExpiredService();
+    }
+
     @VisibleForTesting
     MdnsPacketWriter createMdnsPacketWriter() {
         return new MdnsPacketWriter(DEFAULT_MTU);
@@ -359,11 +392,27 @@
                         listener.onDiscoveryQuerySent(result.second, result.first);
                     }
                 }
+                if (shouldRemoveServiceAfterTtlExpires()) {
+                    Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
+                    while (iter.hasNext()) {
+                        MdnsResponse existingResponse = iter.next();
+                        if (existingResponse.isComplete()
+                                && existingResponse
+                                .getServiceRecord()
+                                .getRemainingTTL(SystemClock.elapsedRealtime())
+                                == 0) {
+                            iter.remove();
+                            for (MdnsServiceBrowserListener listener : listeners) {
+                                listener.onServiceRemoved(
+                                        existingResponse.getServiceInstanceName());
+                            }
+                        }
+                    }
+                }
                 QueryTaskConfig config = this.config.getConfigForNextRun();
                 requestTaskFuture =
                         executor.schedule(
-                                new QueryTask(config), config.timeToRunNextTaskInMs,
-                                TimeUnit.MILLISECONDS);
+                                new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
             }
         }
     }