Add support to update the registered service in place

The current implementation of MdnsAdvertiser doesn't support updating an
existing registration. For an update request, the client needs to
unregister it first, which will trigger an exit message and then
register again, which will trigger an announcement message. There are
some clients that don't want to trigger the exit and announcement
message every time. This CL adds the API to support that use case.

Bug: 309372239
Test: TH
Change-Id: Iabe69a987a11104090082e01969e7595f05504e8
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ee5f25b..1e234fe 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -90,6 +90,7 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.ExecutorProvider;
 import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
 import com.android.server.connectivity.mdns.MdnsFeatureFlags;
 import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
@@ -849,7 +850,9 @@
                             // service type would generate service instance names like
                             // Name._subtype._sub._type._tcp, which is incorrect
                             // (it should be Name._type._tcp).
-                            mAdvertiser.addService(transactionId, serviceInfo, typeSubtype.second);
+                            mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
+                                    typeSubtype.second,
+                                    MdnsAdvertisingOptions.newBuilder().build());
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
                                     serviceInfo.getNetwork());
                         } else {
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 28e3924..fc0e11b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -43,6 +43,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -342,16 +343,16 @@
         }
 
         /**
-         * Add a service.
+         * Add a service to advertise.
          *
          * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
          */
-        void addService(int id, Registration registration) {
+        void addService(int id, @NonNull Registration registration) {
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
                 try {
-                    mAdvertisers.valueAt(i).addService(
-                            id, registration.getServiceInfo(), registration.getSubtype());
+                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+                            registration.getSubtype());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -359,6 +360,17 @@
             }
         }
 
+        /**
+         * Update an already registered service.
+         * The caller is expected to check that the service being updated doesn't change its name
+         */
+        void updateService(int id, @NonNull Registration registration) {
+            mPendingRegistrations.put(id, registration);
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
+            }
+        }
+
         void removeService(int id) {
             mPendingRegistrations.remove(id);
             for (int i = 0; i < mAdvertisers.size(); i++) {
@@ -474,7 +486,8 @@
         @NonNull
         private NsdServiceInfo mServiceInfo;
         @Nullable
-        private final String mSubtype;
+        private String mSubtype;
+
         int mConflictDuringProbingCount;
         int mConflictAfterProbingCount;
 
@@ -485,6 +498,22 @@
         }
 
         /**
+         * Matches between the NsdServiceInfo in the Registration and the provided argument.
+         */
+        public boolean matches(@Nullable NsdServiceInfo newInfo) {
+            return Objects.equals(newInfo.getServiceName(), mOriginalName) && Objects.equals(
+                    newInfo.getServiceType(), mServiceInfo.getServiceType()) && Objects.equals(
+                    newInfo.getNetwork(), mServiceInfo.getNetwork());
+        }
+
+        /**
+         * Update subType for the registration.
+         */
+        public void updateSubtype(@Nullable String subtype) {
+            this.mSubtype = subtype;
+        }
+
+        /**
          * Update the registration to use a different service name, after a conflict was found.
          *
          * @param newInfo New service info to use.
@@ -632,42 +661,68 @@
     }
 
     /**
-     * Add a service to advertise.
+     * Add or update a service to advertise.
+     *
      * @param id A unique ID for the service.
      * @param service The service info to advertise.
      * @param subtype An optional subtype to advertise the service with.
+     * @param advertisingOptions The advertising options.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype) {
+    public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
+            MdnsAdvertisingOptions advertisingOptions) {
         checkThread();
-        if (mRegistrations.get(id) != null) {
-            mSharedLog.e("Adding duplicate registration for " + service);
-            // TODO (b/264986328): add a more specific error code
-            mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
-            return;
-        }
-
-        mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype);
-
+        final Registration existingRegistration = mRegistrations.get(id);
         final Network network = service.getNetwork();
-        final Registration registration = new Registration(service, subtype);
-        final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
-        if (network == null) {
-            // If registering on all networks, no advertiser must have conflicts
-            checkConflictFilter = (net, adv) -> true;
-        } else {
-            // If registering on one network, the matching network advertiser and the one for all
-            // networks must not have conflicts
-            checkConflictFilter = (net, adv) -> net == null || network.equals(net);
-        }
+        Registration registration;
+        if (advertisingOptions.isOnlyUpdate()) {
+            if (existingRegistration == null) {
+                mSharedLog.e("Update non existing registration for " + service);
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            if (!(existingRegistration.matches(service))) {
+                mSharedLog.e("Update request can only update subType, serviceInfo: " + service
+                        + ", existing serviceInfo: " + existingRegistration.getServiceInfo());
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
 
-        updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+            }
+            mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
+                    + " advertisingOptions " + advertisingOptions);
+            registration = existingRegistration;
+            registration.updateSubtype(subtype);
+        } else {
+            if (existingRegistration != null) {
+                mSharedLog.e("Adding duplicate registration for " + service);
+                // TODO (b/264986328): add a more specific error code
+                mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+                return;
+            }
+            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
+                    + " advertisingOptions " + advertisingOptions);
+            registration = new Registration(service, subtype);
+            final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
+            if (network == null) {
+                // If registering on all networks, no advertiser must have conflicts
+                checkConflictFilter = (net, adv) -> true;
+            } else {
+                // If registering on one network, the matching network advertiser and the one
+                // for all networks must not have conflicts
+                checkConflictFilter = (net, adv) -> net == null || network.equals(net);
+            }
+            updateRegistrationUntilNoConflict(checkConflictFilter, registration);
+        }
 
         InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network);
         if (advertiser == null) {
             advertiser = new InterfaceAdvertiserRequest(network);
             mAdvertiserRequests.put(network, advertiser);
         }
-        advertiser.addService(id, registration);
+        if (advertisingOptions.isOnlyUpdate()) {
+            advertiser.updateService(id, registration);
+        } else {
+            advertiser.addService(id, registration);
+        }
         mRegistrations.put(id, registration);
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
new file mode 100644
index 0000000..e7a6ca7
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * API configuration parameters for advertising the mDNS service.
+ *
+ * <p>Use {@link MdnsAdvertisingOptions.Builder} to create {@link MdnsAdvertisingOptions}.
+ *
+ * @hide
+ */
+public class MdnsAdvertisingOptions {
+
+    private static MdnsAdvertisingOptions sDefaultOptions;
+    private final boolean mIsOnlyUpdate;
+
+    /**
+     * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
+     */
+    MdnsAdvertisingOptions(
+            boolean isOnlyUpdate) {
+        this.mIsOnlyUpdate = isOnlyUpdate;
+    }
+
+    /**
+     * Returns a {@link Builder} for {@link MdnsAdvertisingOptions}.
+     */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a default search options.
+     */
+    public static synchronized MdnsAdvertisingOptions getDefaultOptions() {
+        if (sDefaultOptions == null) {
+            sDefaultOptions = newBuilder().build();
+        }
+        return sDefaultOptions;
+    }
+
+    /**
+     * @return {@code true} if the advertising request is an update request.
+     */
+    public boolean isOnlyUpdate() {
+        return mIsOnlyUpdate;
+    }
+
+    @Override
+    public String toString() {
+        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+    }
+
+    /**
+     * A builder to create {@link MdnsAdvertisingOptions}.
+     */
+    public static final class Builder {
+        private boolean mIsOnlyUpdate = false;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets if the advertising request is an update request.
+         */
+        public Builder setIsOnlyUpdate(boolean isOnlyUpdate) {
+            this.mIsOnlyUpdate = isOnlyUpdate;
+            return this;
+        }
+
+        /**
+         * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
+         */
+        public MdnsAdvertisingOptions build() {
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 62c37ad..463df63 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -229,6 +229,18 @@
     }
 
     /**
+     * Update an already registered service without sending exit/re-announcement packet.
+     *
+     * @param id An exiting service id
+     * @param subtype A new subtype
+     */
+    public void updateService(int id, @Nullable String subtype) {
+        // The current implementation is intended to be used in cases where subtypes don't get
+        // announced.
+        mRecordRepository.updateService(id, subtype);
+    }
+
+    /**
      * Start advertising a service.
      *
      * @throws NameConflictException There is already a service being advertised with that name.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index e34778f..48ece68 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -167,7 +167,7 @@
         /**
          * Whether the service is sending exit announcements and will be destroyed soon.
          */
-        public boolean exiting = false;
+        public boolean exiting;
 
         /**
          * The replied query packet count of this service.
@@ -185,13 +185,20 @@
         private boolean isProbing;
 
         /**
+         * Create a ServiceRegistration with only update the subType
+         */
+        ServiceRegistration withSubtype(String newSubType) {
+            return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
+                    newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
+        }
+
+
+        /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
-         *
-         * @param deviceHostname Hostname of the device (for the interface used)
-         * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
+                @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
+                boolean exiting, boolean isProbing) {
             this.serviceInfo = serviceInfo;
             this.subtype = subtype;
 
@@ -266,7 +273,20 @@
             this.allRecords = Collections.unmodifiableList(allRecords);
             this.repliedServiceCount = repliedServiceCount;
             this.sentPacketCount = sentPacketCount;
-            this.isProbing = true;
+            this.isProbing = isProbing;
+            this.exiting = exiting;
+        }
+
+        /**
+         * Create a ServiceRegistration for dns-sd service registration (RFC6763).
+         *
+         * @param deviceHostname Hostname of the device (for the interface used)
+         * @param serviceInfo Service to advertise
+         */
+        ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
+                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
+            this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
+                    false /* exiting */, true /* isProbing */);
         }
 
         void setProbing(boolean probing) {
@@ -305,6 +325,24 @@
     }
 
     /**
+     * Update a service that already registered in the repository.
+     *
+     * @param serviceId An existing service ID.
+     * @param subtype A new subtype
+     * @return
+     */
+    public void updateService(int serviceId, @Nullable String subtype) {
+        final ServiceRegistration existingRegistration = mServices.get(serviceId);
+        if (existingRegistration == null) {
+            throw new IllegalArgumentException(
+                    "Service ID must already exist for an update request: " + serviceId);
+        }
+        final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
+                subtype);
+        mServices.put(serviceId, updatedRegistration);
+    }
+
+    /**
      * Add a service to the repository.
      *
      * This may remove/replace any existing service that used the name added but is exiting.