Report more advertising metrics data

Report more advertising metrics data below when the service is
unregistered.
- Replied request count (sum across interfaces)
- Sent packet count (including announcements and probes)
- Number of conflicts during probing
- Nubmer of conflicts after probing

Bug: 287546772
Test: atest FrameworksNetTestCases NsdManagerTest
Merged-In: I50c54a35dc523422e3a7302c059bbbc38eac5631
Change-Id: I50c54a35dc523422e3a7302c059bbbc38eac5631
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 597c06f..6b03daa 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -24,16 +24,22 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.ConnectivityStatsLog;
 
+import java.util.Random;
+
 /**
  * Class to record the NetworkNsdReported into statsd. Each client should create this class to
  * report its data.
  */
 public class NetworkNsdReportedMetrics {
+    // The upper bound for the random number used in metrics data sampling determines the possible
+    // sample rate.
+    private static final int RANDOM_NUMBER_UPPER_BOUND = 1000;
     // Whether this client is using legacy backend.
     private final boolean mIsLegacy;
     // The client id.
     private final int mClientId;
     private final Dependencies mDependencies;
+    private final Random mRandom;
 
     public NetworkNsdReportedMetrics(boolean isLegacy, int clientId) {
         this(isLegacy, clientId, new Dependencies());
@@ -44,6 +50,7 @@
         mIsLegacy = isLegacy;
         mClientId = clientId;
         mDependencies = dependencies;
+        mRandom = dependencies.makeRandomGenerator();
     }
 
     /**
@@ -67,7 +74,18 @@
                     event.getFoundCallbackCount(),
                     event.getLostCallbackCount(),
                     event.getRepliedRequestsCount(),
-                    event.getSentQueryCount());
+                    event.getSentQueryCount(),
+                    event.getSentPacketCount(),
+                    event.getConflictDuringProbingCount(),
+                    event.getConflictAfterProbingCount(),
+                    event.getRandomNumber());
+        }
+
+        /**
+         * @see Random
+         */
+        public Random makeRandomGenerator() {
+            return new Random();
         }
     }
 
@@ -75,6 +93,7 @@
         final Builder builder = NetworkNsdReported.newBuilder();
         builder.setIsLegacy(mIsLegacy);
         builder.setClientId(mClientId);
+        builder.setRandomNumber(mRandom.nextInt(RANDOM_NUMBER_UPPER_BOUND));
         return builder;
     }
 
@@ -113,14 +132,23 @@
      *
      * @param transactionId The transaction id of service registration.
      * @param durationMs The duration of service stayed registered.
+     * @param repliedRequestsCount The replied request count of this service before unregistered it.
+     * @param sentPacketCount The total sent packet count of this service before unregistered it.
+     * @param conflictDuringProbingCount The number of conflict during probing.
+     * @param conflictAfterProbingCount The number of conflict after probing.
      */
-    public void reportServiceUnregistration(int transactionId, long durationMs) {
+    public void reportServiceUnregistration(int transactionId, long durationMs,
+            int repliedRequestsCount, int sentPacketCount, int conflictDuringProbingCount,
+            int conflictAfterProbingCount) {
         final Builder builder = makeReportedBuilder();
         builder.setTransactionId(transactionId);
         builder.setType(NsdEventType.NET_REGISTER);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_UNREGISTERED);
         builder.setEventDurationMillisec(durationMs);
-        // TODO: Report repliedRequestsCount
+        builder.setRepliedRequestsCount(repliedRequestsCount);
+        builder.setSentPacketCount(sentPacketCount);
+        builder.setConflictDuringProbingCount(conflictDuringProbingCount);
+        builder.setConflictAfterProbingCount(conflictAfterProbingCount);
         mDependencies.statsWrite(builder.build());
     }
 
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 27e97f1..b9acc48 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -29,6 +29,8 @@
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.networkstack.apishim.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
+import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 
@@ -992,14 +994,20 @@
                         // instead of looking at the flag value.
                         final long stopTimeMs = mClock.elapsedRealtime();
                         if (request instanceof AdvertiserClientRequest) {
+                            final AdvertiserMetrics metrics =
+                                    mAdvertiser.getAdvertiserMetrics(transactionId);
                             mAdvertiser.removeService(transactionId);
                             clientInfo.onUnregisterServiceSucceeded(clientRequestId, transactionId,
-                                    request.calculateRequestDurationMs(stopTimeMs));
+                                    request.calculateRequestDurationMs(stopTimeMs), metrics);
                         } else {
                             if (unregisterService(transactionId)) {
                                 clientInfo.onUnregisterServiceSucceeded(clientRequestId,
                                         transactionId,
-                                        request.calculateRequestDurationMs(stopTimeMs));
+                                        request.calculateRequestDurationMs(stopTimeMs),
+                                        new AdvertiserMetrics(NO_PACKET /* repliedRequestsCount */,
+                                                NO_PACKET /* sentPacketCount */,
+                                                0 /* conflictDuringProbingCount */,
+                                                0 /* conflictAfterProbingCount */));
                             } else {
                                 clientInfo.onUnregisterServiceFailed(
                                         clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
@@ -2496,9 +2504,14 @@
                 }
 
                 if (request instanceof AdvertiserClientRequest) {
+                    final AdvertiserMetrics metrics =
+                            mAdvertiser.getAdvertiserMetrics(transactionId);
                     mAdvertiser.removeService(transactionId);
                     mMetrics.reportServiceUnregistration(transactionId,
-                            request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                            request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                            metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                            metrics.mConflictDuringProbingCount,
+                            metrics.mConflictAfterProbingCount);
                     continue;
                 }
 
@@ -2524,7 +2537,11 @@
                     case NsdManager.REGISTER_SERVICE:
                         unregisterService(transactionId);
                         mMetrics.reportServiceUnregistration(transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_PACKET /* repliedRequestsCount */,
+                                NO_PACKET /* sentPacketCount */,
+                                0 /* conflictDuringProbingCount */,
+                                0 /* conflictAfterProbingCount */);
                         break;
                     default:
                         break;
@@ -2663,8 +2680,11 @@
             }
         }
 
-        void onUnregisterServiceSucceeded(int listenerKey, int transactionId, long durationMs) {
-            mMetrics.reportServiceUnregistration(transactionId, durationMs);
+        void onUnregisterServiceSucceeded(int listenerKey, int transactionId, long durationMs,
+                AdvertiserMetrics metrics) {
+            mMetrics.reportServiceUnregistration(transactionId, durationMs,
+                    metrics.mRepliedRequestsCount, metrics.mSentPacketCount,
+                    metrics.mConflictDuringProbingCount, metrics.mConflictAfterProbingCount);
             try {
                 mCb.onUnregisterServiceSucceeded(listenerKey);
             } catch (RemoteException e) {
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 ce05a84..913d233 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
 
 import android.annotation.NonNull;
@@ -180,6 +181,7 @@
                 // (with the old, conflicting, actually not used name as argument... The new
                 // implementation will send callbacks with the new name).
                 registration.mNotifiedRegistrationSuccess = false;
+                registration.mConflictAfterProbingCount++;
 
                 // The service was done probing, just reset it to probing state (RFC6762 9.)
                 forAllAdvertisers(a -> {
@@ -195,6 +197,7 @@
             registration.updateForConflict(
                     registration.makeNewServiceInfoForConflict(1 /* renameCount */),
                     1 /* renameCount */);
+            registration.mConflictDuringProbingCount++;
 
             // Keep renaming if the new name conflicts in local registrations
             updateRegistrationUntilNoConflict((net, adv) -> adv.hasRegistration(registration),
@@ -360,6 +363,22 @@
             }
         }
 
+        int getServiceRepliedRequestsCount(int id) {
+            int repliedRequestsCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                repliedRequestsCount += mAdvertisers.valueAt(i).getServiceRepliedRequestsCount(id);
+            }
+            return repliedRequestsCount;
+        }
+
+        int getSentPacketCount(int id) {
+            int sentPacketCount = NO_PACKET;
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                sentPacketCount += mAdvertisers.valueAt(i).getSentPacketCount(id);
+            }
+            return sentPacketCount;
+        }
+
         @Override
         public void onSocketCreated(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket,
@@ -444,6 +463,8 @@
         private NsdServiceInfo mServiceInfo;
         @Nullable
         private final String mSubtype;
+        int mConflictDuringProbingCount;
+        int mConflictAfterProbingCount;
 
         private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
             this.mOriginalName = serviceInfo.getServiceName();
@@ -555,6 +576,24 @@
                 @NonNull OffloadServiceInfo offloadServiceInfo);
     }
 
+    /**
+     * Data class of avdverting metrics.
+     */
+    public static class AdvertiserMetrics {
+        public final int mRepliedRequestsCount;
+        public final int mSentPacketCount;
+        public final int mConflictDuringProbingCount;
+        public final int mConflictAfterProbingCount;
+
+        public AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount,
+                int conflictDuringProbingCount, int conflictAfterProbingCount) {
+            mRepliedRequestsCount = repliedRequestsCount;
+            mSentPacketCount = sentPacketCount;
+            mConflictDuringProbingCount = conflictDuringProbingCount;
+            mConflictAfterProbingCount = conflictAfterProbingCount;
+        }
+    }
+
     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
             @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
         this(looper, socketProvider, cb, new Dependencies(), sharedLog);
@@ -637,6 +676,34 @@
         }
     }
 
+    /**
+     * Get advertising metrics.
+     *
+     * @param id ID used when registering.
+     * @return The advertising metrics includes replied requests count, send packet count, conflict
+     *         count during/after probing.
+     */
+    public AdvertiserMetrics getAdvertiserMetrics(int id) {
+        checkThread();
+        final Registration registration = mRegistrations.get(id);
+        if (registration == null) {
+            return new AdvertiserMetrics(
+                    NO_PACKET /* repliedRequestsCount */,
+                    NO_PACKET /* sentPacketCount */,
+                    0 /* conflictDuringProbingCount */,
+                    0 /* conflictAfterProbingCount */);
+        }
+        int repliedRequestsCount = NO_PACKET;
+        int sentPacketCount = NO_PACKET;
+        for (int i = 0; i < mAdvertiserRequests.size(); i++) {
+            repliedRequestsCount +=
+                    mAdvertiserRequests.valueAt(i).getServiceRepliedRequestsCount(id);
+            sentPacketCount += mAdvertiserRequests.valueAt(i).getSentPacketCount(id);
+        }
+        return new AdvertiserMetrics(repliedRequestsCount, sentPacketCount,
+                registration.mConflictDuringProbingCount, registration.mConflictAfterProbingCount);
+    }
+
     private static <K, V> boolean any(@NonNull ArrayMap<K, V> map,
             @NonNull BiPredicate<K, V> predicate) {
         for (int i = 0; i < map.size(); i++) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 0c32cf1..1251170 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -37,6 +37,7 @@
     public static final int FLAG_TRUNCATED = 0x0200;
     public static final int QCLASS_INTERNET = 0x0001;
     public static final int QCLASS_UNICAST = 0x8000;
+    public static final int NO_PACKET = 0;
     public static final String SUBTYPE_LABEL = "_sub";
     public static final String SUBTYPE_PREFIX = "_";
     private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
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 61a2c5e..6454959 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkAddress;
@@ -93,8 +95,11 @@
     /**
      * Callbacks from {@link MdnsProber}.
      */
-    private class ProbingCallback implements
-            PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+    private class ProbingCallback implements PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+        @Override
+        public void onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount) {
+            mRecordRepository.onProbingSent(info.getServiceId(), sentPacketCount);
+        }
         @Override
         public void onFinished(MdnsProber.ProbingInfo info) {
             final MdnsAnnouncer.AnnouncementInfo announcementInfo;
@@ -118,8 +123,8 @@
      */
     private class AnnouncingCallback implements PacketRepeaterCallback<BaseAnnouncementInfo> {
         @Override
-        public void onSent(int index, @NonNull BaseAnnouncementInfo info) {
-            mRecordRepository.onAdvertisementSent(info.getServiceId());
+        public void onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount) {
+            mRecordRepository.onAdvertisementSent(info.getServiceId(), sentPacketCount);
         }
 
         @Override
@@ -260,6 +265,22 @@
     }
 
     /**
+     * Get the replied request count from given service id.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getServiceRepliedRequestsCount(id);
+    }
+
+    /**
+     * Get the total sent packet count from given service id.
+     */
+    public int getSentPacketCount(int id) {
+        if (!mRecordRepository.hasActiveService(id)) return NO_PACKET;
+        return mRecordRepository.getSentPacketCount(id);
+    }
+
+    /**
      * Update interface addresses used to advertise.
      *
      * This causes new address records to be announced.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index 644560c..12ed139 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -59,7 +59,7 @@
         /**
          * Called when a packet was sent.
          */
-        default void onSent(int index, @NonNull T info) {}
+        default void onSent(int index, @NonNull T info, int sentPacketCount) {}
 
         /**
          * Called when the {@link MdnsPacketRepeater} is done sending packets.
@@ -114,9 +114,10 @@
             }
             // Send to both v4 and v6 addresses; the reply sender will take care of ignoring the
             // send when the socket has not joined the relevant group.
+            int sentPacketCount = 0;
             for (InetSocketAddress destination : ALL_ADDRS) {
                 try {
-                    mReplySender.sendNow(packet, destination);
+                    sentPacketCount += mReplySender.sendNow(packet, destination);
                 } catch (IOException e) {
                     mSharedLog.e("Error sending packet to " + destination, e);
                 }
@@ -135,7 +136,7 @@
 
             // Call onSent after scheduling the next run, to allow the callback to cancel it
             if (mCb != null) {
-                mCb.onSent(index, request);
+                mCb.onSent(index, request, sentPacketCount);
             }
         }
     }
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 e25d7e1..1fb4d90 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TargetApi;
@@ -175,13 +177,23 @@
         public boolean exiting = false;
 
         /**
+         * The replied query packet count of this service.
+         */
+        public int repliedServiceCount = NO_PACKET;
+
+        /**
+         * The sent packet count of this service (including announcements and probes).
+         */
+        public int sentPacketCount = NO_PACKET;
+
+        /**
          * 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) {
+                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
             this.serviceInfo = serviceInfo;
             this.subtype = subtype;
 
@@ -254,6 +266,8 @@
                     true /* sharedName */, true /* probing */));
 
             this.allRecords = Collections.unmodifiableList(allRecords);
+            this.repliedServiceCount = repliedServiceCount;
+            this.sentPacketCount = sentPacketCount;
         }
 
         void setProbing(boolean probing) {
@@ -316,7 +330,8 @@
         }
 
         final ServiceRegistration registration = new ServiceRegistration(
-                mDeviceHostname, serviceInfo, subtype);
+                mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+                NO_PACKET /* sentPacketCount */);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -406,6 +421,24 @@
     }
 
     /**
+     * @return The replied request count of the service.
+     */
+    public int getServiceRepliedRequestsCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.repliedServiceCount;
+    }
+
+    /**
+     * @return The total sent packet count of the service.
+     */
+    public int getSentPacketCount(int id) {
+        final ServiceRegistration service = mServices.get(id);
+        if (service == null) return NO_PACKET;
+        return service.sentPacketCount;
+    }
+
+    /**
      * Remove all services from the repository
      * @return IDs of the removed services
      */
@@ -472,9 +505,12 @@
             for (int i = 0; i < mServices.size(); i++) {
                 final ServiceRegistration registration = mServices.valueAt(i);
                 if (registration.exiting) continue;
-                addReplyFromService(question, registration.allRecords, registration.ptrRecords,
+                if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
                         registration.srvRecord, registration.txtRecord, replyUnicast, now,
-                        answerInfo, additionalAnswerRecords);
+                        answerInfo, additionalAnswerRecords)) {
+                    registration.repliedServiceCount++;
+                    registration.sentPacketCount++;
+                }
             }
         }
 
@@ -527,7 +563,7 @@
     /**
      * Add answers and additional answers for a question, from a ServiceRegistration.
      */
-    private void addReplyFromService(@NonNull MdnsRecord question,
+    private boolean addReplyFromService(@NonNull MdnsRecord question,
             @NonNull List<RecordInfo<?>> serviceRecords,
             @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
             @Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@@ -596,7 +632,7 @@
         }
 
         // No more records to add if no answer
-        if (answerInfo.size() == answersStartIndex) return;
+        if (answerInfo.size() == answersStartIndex) return false;
 
         final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
         // RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
@@ -626,6 +662,7 @@
         addNsecRecordsForUniqueNames(additionalAnswerRecords,
                 answerInfo.listIterator(answersStartIndex),
                 additionalAnswerInfo.listIterator());
+        return true;
     }
 
     /**
@@ -862,8 +899,8 @@
         final ServiceRegistration existing = mServices.get(serviceId);
         if (existing == null) return null;
 
-        final ServiceRegistration newService = new ServiceRegistration(
-                mDeviceHostname, newInfo, existing.subtype);
+        final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
+                existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService.srvRecord.record);
     }
@@ -871,7 +908,7 @@
     /**
      * Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
      */
-    public void onAdvertisementSent(int serviceId) {
+    public void onAdvertisementSent(int serviceId, int sentPacketCount) {
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) return;
 
@@ -880,9 +917,20 @@
             record.lastSentTimeMs = now;
             record.lastAdvertisedTimeMs = now;
         }
+        registration.sentPacketCount += sentPacketCount;
     }
 
     /**
+     * Called when {@link MdnsAdvertiser} sent a probing for the given service.
+     */
+    public void onProbingSent(int serviceId, int sentPacketCount) {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) return;
+        registration.sentPacketCount += sentPacketCount;
+    }
+
+
+    /**
      * Compute:
      * 2001:db8::1 --> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa
      *
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index cf7a464..71057fb 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -44,6 +44,9 @@
 public class MdnsReplySender {
     private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final int MSG_SEND = 1;
+    private static final int PACKET_NOT_SENT = 0;
+    private static final int PACKET_SENT = 1;
+
     @NonNull
     private final MdnsInterfaceSocket mSocket;
     @NonNull
@@ -79,16 +82,17 @@
      *
      * Must be called on the looper thread used by the {@link MdnsReplySender}.
      */
-    public void sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
+    public int sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
             throws IOException {
         ensureRunningOnHandlerThread(mHandler);
         if (!((destination.getAddress() instanceof Inet6Address && mSocket.hasJoinedIpv6())
                 || (destination.getAddress() instanceof Inet4Address && mSocket.hasJoinedIpv4()))) {
             // Skip sending if the socket has not joined the v4/v6 group (there was no address)
-            return;
+            return PACKET_NOT_SENT;
         }
         final byte[] outBuffer = MdnsUtils.createRawDnsPacket(mPacketCreationBuffer, packet);
         mSocket.send(new DatagramPacket(outBuffer, 0, outBuffer.length, destination));
+        return PACKET_SENT;
     }
 
     /** Get the packetCreationBuffer */
diff --git a/service/src/com/android/metrics/stats.proto b/service/src/com/android/metrics/stats.proto
index 99afb90..ecc0377 100644
--- a/service/src/com/android/metrics/stats.proto
+++ b/service/src/com/android/metrics/stats.proto
@@ -64,6 +64,18 @@
 
   // Record sent query count before stopped discovery
   optional int32 sent_query_count = 12;
+
+  // Record sent packet count before unregistered service
+  optional int32 sent_packet_count = 13;
+
+  // Record number of conflict during probing
+  optional int32 conflict_during_probing_count = 14;
+
+  // Record number of conflict after probing
+  optional int32 conflict_after_probing_count = 15;
+
+  // The random number between 0 ~ 999 for sampling
+  optional int32 random_number = 16;
 }
 
 /**
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index 7f893df..a82e29b 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -21,12 +21,15 @@
 import android.stats.connectivity.NsdEventType
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import java.util.Random
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 
@@ -34,6 +37,12 @@
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class NetworkNsdReportedMetricsTest {
     private val deps = mock(NetworkNsdReportedMetrics.Dependencies::class.java)
+    private val random = mock(Random::class.java)
+
+    @Before
+    fun setUp() {
+        doReturn(random).`when`(deps).makeRandomGenerator()
+    }
 
     @Test
     fun testReportServiceRegistrationSucceeded() {
@@ -80,8 +89,13 @@
         val clientId = 99
         val transactionId = 100
         val durationMs = 10L
+        val repliedRequestsCount = 25
+        val sentPacketCount = 50
+        val conflictDuringProbingCount = 2
+        val conflictAfterProbingCount = 1
         val metrics = NetworkNsdReportedMetrics(true /* isLegacy */, clientId, deps)
-        metrics.reportServiceUnregistration(transactionId, durationMs)
+        metrics.reportServiceUnregistration(transactionId, durationMs, repliedRequestsCount,
+                sentPacketCount, conflictDuringProbingCount, conflictAfterProbingCount)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -92,6 +106,10 @@
             assertEquals(NsdEventType.NET_REGISTER, it.type)
             assertEquals(MdnsQueryResult.MQR_SERVICE_UNREGISTERED, it.queryResult)
             assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(repliedRequestsCount, it.repliedRequestsCount)
+            assertEquals(sentPacketCount, it.sentPacketCount)
+            assertEquals(conflictDuringProbingCount, it.conflictDuringProbingCount)
+            assertEquals(conflictAfterProbingCount, it.conflictAfterProbingCount)
         }
     }
 
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 695cfe8..44ed02a 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -1222,6 +1222,8 @@
         verify(mMockMDnsM).stopOperation(legacyIdCaptor.getValue());
         verify(mAdvertiser, never()).removeService(anyInt());
 
+        doReturn(mock(MdnsAdvertiser.AdvertiserMetrics.class))
+                .when(mAdvertiser).getAdvertiserMetrics(anyInt());
         client.unregisterService(regListenerWithFeature);
         waitForIdle();
         verify(mAdvertiser).removeService(serviceIdCaptor.getValue());
@@ -1312,14 +1314,20 @@
                 new NsdServiceInfo(regInfo.getServiceName(), null))));
         verify(mMetrics).reportServiceRegistrationSucceeded(regId, 10L /* durationMs */);
 
+        final MdnsAdvertiser.AdvertiserMetrics metrics = new MdnsAdvertiser.AdvertiserMetrics(
+                50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
         doReturn(TEST_TIME_MS + 100L).when(mClock).elapsedRealtime();
+        doReturn(metrics).when(mAdvertiser).getAdvertiserMetrics(regId);
         client.unregisterService(regListener);
         waitForIdle();
         verify(mAdvertiser).removeService(idCaptor.getValue());
         verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
                 argThat(info -> matches(info, regInfo)));
         verify(mSocketProvider, timeout(TIMEOUT_MS)).requestStopWhenInactive();
-        verify(mMetrics).reportServiceUnregistration(regId, 100L /* durationMs */);
+        verify(mMetrics).reportServiceUnregistration(regId, 100L /* durationMs */,
+                50 /* repliedRequestCount */, 100 /* sentPacketCount */,
+                3 /* conflictDuringProbingCount */, 2 /* conflictAfterProbingCount */);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 862a9ec..8b10e0b 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -33,7 +33,10 @@
 import com.android.testutils.waitForIdle
 import java.net.NetworkInterface
 import java.util.Objects
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
 import org.junit.After
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -206,6 +209,18 @@
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
+        // Service is conflicted.
+        postSync { intAdvCbCaptor.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+
+        // Verify the metrics data
+        doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(40).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(25, metrics.mRepliedRequestsCount)
+        assertEquals(40, metrics.mSentPacketCount)
+        assertEquals(0, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
+
         doReturn(TEST_OFFLOAD_PACKET2).`when`(mockInterfaceAdvertiser1)
             .getRawOffloadPayload(
                 SERVICE_ID_1
@@ -265,6 +280,22 @@
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
                 argThat { it.matches(ALL_NETWORKS_SERVICE) })
 
+        // Services are conflicted.
+        postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+        postSync { intAdvCbCaptor2.value.onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1) }
+
+        // Verify the metrics data
+        doReturn(10).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(5).`when`(mockInterfaceAdvertiser2).getServiceRepliedRequestsCount(SERVICE_ID_1)
+        doReturn(22).`when`(mockInterfaceAdvertiser1).getSentPacketCount(SERVICE_ID_1)
+        doReturn(12).`when`(mockInterfaceAdvertiser2).getSentPacketCount(SERVICE_ID_1)
+        val metrics = postReturn { advertiser.getAdvertiserMetrics(SERVICE_ID_1) }
+        assertEquals(15, metrics.mRepliedRequestsCount)
+        assertEquals(34, metrics.mSentPacketCount)
+        assertEquals(2, metrics.mConflictDuringProbingCount)
+        assertEquals(1, metrics.mConflictAfterProbingCount)
+
         // Unregister the service
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockInterfaceAdvertiser1).removeService(SERVICE_ID_1)
@@ -376,6 +407,14 @@
         handler.post(r)
         handler.waitForIdle(TIMEOUT_MS)
     }
+
+    private fun <T> postReturn(r: (() -> T)): T {
+        val future = CompletableFuture<T>()
+        handler.post {
+            future.complete(r())
+        }
+        return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
 }
 
 // NsdServiceInfo does not implement equals; this is useful to use in argument matchers
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index 12faa50..c39ee1e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -253,7 +253,7 @@
 
         val captor = ArgumentCaptor.forClass(DatagramPacket::class.java)
         repeat(FIRST_ANNOUNCES_COUNT) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request, 1 /* sentPacketCount */)
             verify(socket, atLeast(i + 1)).send(any())
             val now = SystemClock.elapsedRealtime()
             assertTrue(now > timeStart + startDelay + i * FIRST_ANNOUNCES_DELAY)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 5ca4dd6..f284819 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -92,7 +92,7 @@
 
     private fun assertProbesSent(probeInfo: TestProbeInfo, expectedHex: String) {
         repeat(probeInfo.numSends) { i ->
-            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo)
+            verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, probeInfo, 1 /* sentPacketCount */)
             // If the probe interval is short, more than (i+1) probes may have been sent already
             verify(socket, atLeast(i + 1)).send(any())
         }
@@ -190,7 +190,7 @@
         prober.startProbing(probeInfo)
 
         // Expect the initial probe
-        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo)
+        verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(0, probeInfo, 1 /* sentPacketCount */)
 
         // Stop probing
         val stopResult = CompletableFuture<Boolean>()
@@ -200,7 +200,7 @@
 
         // Wait for a bit (more than the probe delay) to ensure no more probes were sent
         Thread.sleep(SHORT_TIMEOUT_MS * 2)
-        verify(cb, never()).onSent(1, probeInfo)
+        verify(cb, never()).onSent(1, probeInfo, 1 /* sentPacketCount */)
         verify(cb, never()).onFinished(probeInfo)
 
         // Only one sent packet
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 56fbdf0..af47b1c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -155,7 +155,7 @@
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
         repository.onProbingSucceeded(probingInfo)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         repository.exitService(TEST_SERVICE_ID_1)
@@ -166,7 +166,7 @@
     fun testExitAnnouncements() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
@@ -195,7 +195,7 @@
     fun testExitAnnouncements_WithSubtype() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
         assertNotNull(exitAnnouncement)
@@ -230,7 +230,7 @@
     fun testExitingServiceReAdded() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         repository.exitService(TEST_SERVICE_ID_1)
 
         assertEquals(TEST_SERVICE_ID_1,
@@ -246,7 +246,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
         val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
                 TEST_SUBTYPE)
-        repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+        repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         val packet = announcementInfo.getPacket(0)
 
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
@@ -657,6 +657,34 @@
         // Above records are identical to the actual registrations: no conflict
         assertEquals(emptySet(), repository.getConflictingServices(packet))
     }
+
+    @Test
+    fun testGetServiceRepliedRequestsCount() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        // Verify that there is no packet replied.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        val questions = listOf(MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                // TTL and data is empty for a question
+                0L /* ttlMillis */,
+                null /* pointer */))
+        val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+                listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+        // Reply to the question and verify there is one packet replied.
+        val reply = repository.getReply(query, src)
+        assertNotNull(reply)
+        assertEquals(1, repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_1))
+
+        // No package replied for unknown service.
+        assertEquals(MdnsConstants.NO_PACKET,
+                repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_2))
+    }
 }
 
 private fun MdnsRecordRepository.initWithService(