Report discovery metrics data

Collect information on discovery start, failure, and stop events,
then use this data to build metrics and report them.

Bug: 287546772
Test: atest FrameworksNetTestCases NsdManagerTest
Change-Id: I2f6a718024870f9658cfeabe8995624a3ad81f05
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 4594f71..71788d2 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -123,4 +123,54 @@
         // TODO: Report repliedRequestsCount
         mDependencies.statsWrite(builder.build());
     }
+
+    /**
+     * Report service discovery started metric data.
+     *
+     * @param transactionId The transaction id of service discovery.
+     */
+    public void reportServiceDiscoveryStarted(int transactionId) {
+        final Builder builder = makeReportedBuilder();
+        builder.setTransactionId(transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STARTED);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service discovery failed metric data.
+     *
+     * @param transactionId The transaction id of service discovery.
+     * @param durationMs The duration of service discovery failed.
+     */
+    public void reportServiceDiscoveryFailed(int transactionId, long durationMs) {
+        final Builder builder = makeReportedBuilder();
+        builder.setTransactionId(transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_FAILED);
+        builder.setEventDurationMillisec(durationMs);
+        mDependencies.statsWrite(builder.build());
+    }
+
+    /**
+     * Report service discovery stop metric data.
+     *
+     * @param transactionId The transaction id of service discovery.
+     * @param durationMs The duration of discovering services.
+     * @param foundCallbackCount The count of found service callbacks before stop discovery.
+     * @param lostCallbackCount The count of lost service callbacks before stop discovery.
+     * @param servicesCount The count of found services.
+     */
+    public void reportServiceDiscoveryStop(int transactionId, long durationMs,
+            int foundCallbackCount, int lostCallbackCount, int servicesCount) {
+        final Builder builder = makeReportedBuilder();
+        builder.setTransactionId(transactionId);
+        builder.setType(NsdEventType.NET_DISCOVER);
+        builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP);
+        builder.setEventDurationMillisec(durationMs);
+        builder.setFoundCallbackCount(foundCallbackCount);
+        builder.setLostCallbackCount(lostCallbackCount);
+        builder.setFoundServiceCount(servicesCount);
+        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 f7edbe4..786a181 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -104,6 +104,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -168,7 +169,9 @@
     public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final long CLEANUP_DELAY_MS = 10000;
     private static final int IFACE_IDX_ANY = 0;
-    private static final int NO_TRANSACTION = -1;
+    private static final int MAX_SERVICES_COUNT_METRIC_PER_CLIENT = 100;
+    @VisibleForTesting
+    static final int NO_TRANSACTION = -1;
     private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
 
     private final Context mContext;
@@ -239,7 +242,8 @@
         }
     }
 
-    private static class MdnsListener implements MdnsServiceBrowserListener {
+    @VisibleForTesting
+    static class MdnsListener implements MdnsServiceBrowserListener {
         protected final int mClientRequestId;
         protected final int mTransactionId;
         @NonNull
@@ -583,7 +587,7 @@
                     case NsdManager.DISCOVER_SERVICES:
                         cInfo = getClientInfoForReply(msg);
                         if (cInfo != null) {
-                            cInfo.onDiscoverServicesFailed(
+                            cInfo.onDiscoverServicesFailedImmediately(
                                     clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
                         }
                        break;
@@ -680,7 +684,7 @@
             private void storeLegacyRequestMap(int clientRequestId, int transactionId,
                     ClientInfo clientInfo, int what) {
                 clientInfo.mClientRequests.put(clientRequestId, new LegacyClientRequest(
-                        transactionId, what, mClock, mClock.elapsedRealtime()));
+                        transactionId, what, mClock.elapsedRealtime()));
                 mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 // Remove the cleanup event because here comes a new request.
                 cancelStop();
@@ -689,7 +693,7 @@
             private void storeAdvertiserRequestMap(int clientRequestId, int transactionId,
                     ClientInfo clientInfo, @Nullable Network requestedNetwork) {
                 clientInfo.mClientRequests.put(clientRequestId, new AdvertiserClientRequest(
-                        transactionId, requestedNetwork, mClock, mClock.elapsedRealtime()));
+                        transactionId, requestedNetwork, mClock.elapsedRealtime()));
                 mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 updateMulticastLock();
             }
@@ -713,8 +717,7 @@
                     MdnsListener listener, ClientInfo clientInfo,
                     @Nullable Network requestedNetwork) {
                 clientInfo.mClientRequests.put(clientRequestId, new DiscoveryManagerRequest(
-                        transactionId, listener, requestedNetwork, mClock,
-                        mClock.elapsedRealtime()));
+                        transactionId, listener, requestedNetwork, mClock.elapsedRealtime()));
                 mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 updateMulticastLock();
             }
@@ -758,7 +761,7 @@
                         }
 
                         if (requestLimitReached(clientInfo)) {
-                            clientInfo.onDiscoverServicesFailed(
+                            clientInfo.onDiscoverServicesFailedImmediately(
                                     clientRequestId, NsdManager.FAILURE_MAX_LIMIT);
                             break;
                         }
@@ -773,8 +776,8 @@
                                 || mDeps.isMdnsDiscoveryManagerEnabled(mContext)
                                 || useDiscoveryManagerForType(serviceType)) {
                             if (serviceType == null) {
-                                clientInfo.onDiscoverServicesFailed(clientRequestId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                clientInfo.onDiscoverServicesFailedImmediately(
+                                        clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
                                 break;
                             }
 
@@ -796,7 +799,8 @@
                                     listenServiceType, listener, optionsBuilder.build());
                             storeDiscoveryManagerRequestMap(clientRequestId, transactionId,
                                     listener, clientInfo, info.getNetwork());
-                            clientInfo.onDiscoverServicesStarted(clientRequestId, info);
+                            clientInfo.onDiscoverServicesStarted(
+                                    clientRequestId, info, transactionId);
                             clientInfo.log("Register a DiscoveryListener " + transactionId
                                     + " for service type:" + listenServiceType);
                         } else {
@@ -808,11 +812,12 @@
                                 }
                                 storeLegacyRequestMap(
                                         clientRequestId, transactionId, clientInfo, msg.what);
-                                clientInfo.onDiscoverServicesStarted(clientRequestId, info);
+                                clientInfo.onDiscoverServicesStarted(
+                                        clientRequestId, info, transactionId);
                             } else {
                                 stopServiceDiscovery(transactionId);
-                                clientInfo.onDiscoverServicesFailed(clientRequestId,
-                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                clientInfo.onDiscoverServicesFailedImmediately(
+                                        clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
                             }
                         }
                         break;
@@ -842,12 +847,12 @@
                         if (request instanceof DiscoveryManagerRequest) {
                             stopDiscoveryManagerRequest(
                                     request, clientRequestId, transactionId, clientInfo);
-                            clientInfo.onStopDiscoverySucceeded(clientRequestId);
+                            clientInfo.onStopDiscoverySucceeded(clientRequestId, request);
                             clientInfo.log("Unregister the DiscoveryListener " + transactionId);
                         } else {
                             removeRequestMap(clientRequestId, transactionId, clientInfo);
                             if (stopServiceDiscovery(transactionId)) {
-                                clientInfo.onStopDiscoverySucceeded(clientRequestId);
+                                clientInfo.onStopDiscoverySucceeded(clientRequestId, request);
                             } else {
                                 clientInfo.onStopDiscoveryFailed(
                                         clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
@@ -942,14 +947,16 @@
                         // Note isMdnsAdvertiserEnabled may have changed to false at this point,
                         // so this needs to check the type of the original request to unregister
                         // instead of looking at the flag value.
+                        final long stopTimeMs = mClock.elapsedRealtime();
                         if (request instanceof AdvertiserClientRequest) {
                             mAdvertiser.removeService(transactionId);
                             clientInfo.onUnregisterServiceSucceeded(clientRequestId, transactionId,
-                                    request.calculateRequestDurationMs());
+                                    request.calculateRequestDurationMs(stopTimeMs));
                         } else {
                             if (unregisterService(transactionId)) {
                                 clientInfo.onUnregisterServiceSucceeded(clientRequestId,
-                                        transactionId, request.calculateRequestDurationMs());
+                                        transactionId,
+                                        request.calculateRequestDurationMs(stopTimeMs));
                             } else {
                                 clientInfo.onUnregisterServiceFailed(
                                         clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
@@ -1174,6 +1181,11 @@
                             code, transactionId));
                     return false;
                 }
+                final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+                if (request == null) {
+                    Log.e(TAG, "Unknown client request. clientRequestId=" + clientRequestId);
+                    return false;
+                }
                 if (DBG) {
                     Log.d(TAG, String.format(
                             "MDns service event code:%d transactionId=%d", code, transactionId));
@@ -1198,7 +1210,8 @@
                             break;
                         }
                         setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
-                        clientInfo.onServiceFound(clientRequestId, servInfo);
+
+                        clientInfo.onServiceFound(clientRequestId, servInfo, request);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_LOST: {
@@ -1212,29 +1225,27 @@
                         // TODO: avoid returning null in that case, possibly by remembering
                         // found services on the same interface index and their network at the time
                         setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
-                        clientInfo.onServiceLost(clientRequestId, servInfo);
+                        clientInfo.onServiceLost(clientRequestId, servInfo, request);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
-                        clientInfo.onDiscoverServicesFailed(
-                                clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        clientInfo.onDiscoverServicesFailed(clientRequestId,
+                                NsdManager.FAILURE_INTERNAL_ERROR, transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     case IMDnsEventListener.SERVICE_REGISTERED: {
                         final RegistrationInfo info = (RegistrationInfo) obj;
                         final String name = info.serviceName;
                         servInfo = new NsdServiceInfo(name, null /* serviceType */);
-                        final ClientRequest request =
-                                clientInfo.mClientRequests.get(clientRequestId);
                         clientInfo.onRegisterServiceSucceeded(clientRequestId, servInfo,
-                                transactionId, request.calculateRequestDurationMs());
+                                transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     }
                     case IMDnsEventListener.SERVICE_REGISTRATION_FAILED:
-                        final ClientRequest request =
-                                clientInfo.mClientRequests.get(clientRequestId);
                         clientInfo.onRegisterServiceFailed(clientRequestId,
                                 NsdManager.FAILURE_INTERNAL_ERROR, transactionId,
-                                request.calculateRequestDurationMs());
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     case IMDnsEventListener.SERVICE_RESOLVED: {
                         final ResolutionInfo info = (ResolutionInfo) obj;
@@ -1390,20 +1401,19 @@
                 mServiceLogs.log(String.format(
                         "MdnsDiscoveryManager event code=%s transactionId=%d",
                         NsdManager.nameOf(code), transactionId));
+                final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
+                if (request == null) {
+                    Log.e(TAG, "Unknown client request. clientRequestId=" + clientRequestId);
+                    return false;
+                }
                 switch (code) {
                     case NsdManager.SERVICE_FOUND:
-                        clientInfo.onServiceFound(clientRequestId, info);
+                        clientInfo.onServiceFound(clientRequestId, info, request);
                         break;
                     case NsdManager.SERVICE_LOST:
-                        clientInfo.onServiceLost(clientRequestId, info);
+                        clientInfo.onServiceLost(clientRequestId, info, request);
                         break;
                     case NsdManager.RESOLVE_SERVICE_SUCCEEDED: {
-                        final ClientRequest request =
-                                clientInfo.mClientRequests.get(clientRequestId);
-                        if (request == null) {
-                            Log.e(TAG, "Unknown client request in RESOLVE_SERVICE_SUCCEEDED");
-                            break;
-                        }
                         final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
                         info.setPort(serviceInfo.getPort());
 
@@ -1856,8 +1866,8 @@
             // historical behavior.
             final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
             final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
-            clientInfo.onRegisterServiceSucceeded(
-                    clientRequestId, cbInfo, transactionId, request.calculateRequestDurationMs());
+            clientInfo.onRegisterServiceSucceeded(clientRequestId, cbInfo, transactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
         }
 
         @Override
@@ -1869,7 +1879,7 @@
             if (clientRequestId < 0) return;
             final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
             clientInfo.onRegisterServiceFailed(clientRequestId, errorCode, transactionId,
-                    request.calculateRequestDurationMs());
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
         }
 
         @Override
@@ -2186,27 +2196,49 @@
 
     private abstract static class ClientRequest {
         private final int mTransactionId;
-        private final Clock mClock;
         private final long mStartTimeMs;
+        private int mFoundServiceCount = 0;
+        private int mLostServiceCount = 0;
+        private final Set<String> mServices = new ArraySet<>();
 
-        private ClientRequest(int transactionId, @NonNull Clock clock, long startTimeMs) {
+        private ClientRequest(int transactionId, long startTimeMs) {
             mTransactionId = transactionId;
-            mClock = clock;
             mStartTimeMs = startTimeMs;
         }
 
-        public long calculateRequestDurationMs() {
-            final long stopTimeMs = mClock.elapsedRealtime();
+        public long calculateRequestDurationMs(long stopTimeMs) {
             return stopTimeMs - mStartTimeMs;
         }
+
+        public void onServiceFound(String serviceName) {
+            mFoundServiceCount++;
+            if (mServices.size() <= MAX_SERVICES_COUNT_METRIC_PER_CLIENT) {
+                mServices.add(serviceName);
+            }
+        }
+
+        public void onServiceLost() {
+            mLostServiceCount++;
+        }
+
+        public int getFoundServiceCount() {
+            return mFoundServiceCount;
+        }
+
+        public int getLostServiceCount() {
+            return mLostServiceCount;
+        }
+
+        public int getServicesCount() {
+            return mServices.size();
+        }
     }
 
     private static class LegacyClientRequest extends ClientRequest {
         private final int mRequestCode;
 
-        private LegacyClientRequest(int transactionId, int requestCode, @NonNull Clock clock,
-                long startTimeMs) {
-            super(transactionId, clock, startTimeMs);
+        private LegacyClientRequest(int transactionId, int requestCode, long startTimeMs) {
+            super(transactionId, startTimeMs);
             mRequestCode = requestCode;
         }
     }
@@ -2216,8 +2248,8 @@
         private final Network mRequestedNetwork;
 
         private JavaBackendClientRequest(int transactionId, @Nullable Network requestedNetwork,
-                @NonNull Clock clock, long startTimeMs) {
-            super(transactionId, clock, startTimeMs);
+                long startTimeMs) {
+            super(transactionId, startTimeMs);
             mRequestedNetwork = requestedNetwork;
         }
 
@@ -2229,8 +2261,8 @@
 
     private static class AdvertiserClientRequest extends JavaBackendClientRequest {
         private AdvertiserClientRequest(int transactionId, @Nullable Network requestedNetwork,
-                @NonNull Clock clock, long startTimeMs) {
-            super(transactionId, requestedNetwork, clock, startTimeMs);
+                long startTimeMs) {
+            super(transactionId, requestedNetwork, startTimeMs);
         }
     }
 
@@ -2239,8 +2271,8 @@
         private final MdnsListener mListener;
 
         private DiscoveryManagerRequest(int transactionId, @NonNull MdnsListener listener,
-                @Nullable Network requestedNetwork, @NonNull Clock clock, long startTimeMs) {
-            super(transactionId, requestedNetwork, clock, startTimeMs);
+                @Nullable Network requestedNetwork, long startTimeMs) {
+            super(transactionId, requestedNetwork, startTimeMs);
             mListener = listener;
         }
     }
@@ -2303,11 +2335,12 @@
             mIsPreSClient = true;
         }
 
-        private void unregisterMdnsListenerFromRequest(ClientRequest request) {
+        private MdnsListener unregisterMdnsListenerFromRequest(ClientRequest request) {
             final MdnsListener listener =
                     ((DiscoveryManagerRequest) request).mListener;
             mMdnsDiscoveryManager.unregisterListener(
                     listener.getListenedServiceType(), listener);
+            return listener;
         }
 
         // Remove any pending requests from the global map when we get rid of a client,
@@ -2327,14 +2360,21 @@
                 }
 
                 if (request instanceof DiscoveryManagerRequest) {
-                    unregisterMdnsListenerFromRequest(request);
+                    final MdnsListener listener = unregisterMdnsListenerFromRequest(request);
+                    if (listener instanceof DiscoveryListener) {
+                        mMetrics.reportServiceDiscoveryStop(transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getFoundServiceCount(),
+                                request.getLostServiceCount(),
+                                request.getServicesCount());
+                    }
                     continue;
                 }
 
                 if (request instanceof AdvertiserClientRequest) {
                     mAdvertiser.removeService(transactionId);
-                    mMetrics.reportServiceUnregistration(
-                            transactionId, request.calculateRequestDurationMs());
+                    mMetrics.reportServiceUnregistration(transactionId,
+                            request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                     continue;
                 }
 
@@ -2345,14 +2385,19 @@
                 switch (((LegacyClientRequest) request).mRequestCode) {
                     case NsdManager.DISCOVER_SERVICES:
                         stopServiceDiscovery(transactionId);
+                        mMetrics.reportServiceDiscoveryStop(transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getFoundServiceCount(),
+                                request.getLostServiceCount(),
+                                request.getServicesCount());
                         break;
                     case NsdManager.RESOLVE_SERVICE:
                         stopResolveService(transactionId);
                         break;
                     case NsdManager.REGISTER_SERVICE:
                         unregisterService(transactionId);
-                        mMetrics.reportServiceUnregistration(
-                                transactionId, request.calculateRequestDurationMs());
+                        mMetrics.reportServiceUnregistration(transactionId,
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
                         break;
                     default:
                         break;
@@ -2396,15 +2441,21 @@
             mClientLogs.log(message);
         }
 
-        void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+        void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info, int transactionId) {
+            mMetrics.reportServiceDiscoveryStarted(transactionId);
             try {
                 mCb.onDiscoverServicesStarted(listenerKey, info);
             } catch (RemoteException e) {
                 Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
             }
         }
+        void onDiscoverServicesFailedImmediately(int listenerKey, int error) {
+            onDiscoverServicesFailed(listenerKey, error, NO_TRANSACTION, 0L /* durationMs */);
+        }
 
-        void onDiscoverServicesFailed(int listenerKey, int error) {
+        void onDiscoverServicesFailed(int listenerKey, int error, int transactionId,
+                long durationMs) {
+            mMetrics.reportServiceDiscoveryFailed(transactionId, durationMs);
             try {
                 mCb.onDiscoverServicesFailed(listenerKey, error);
             } catch (RemoteException e) {
@@ -2412,7 +2463,8 @@
             }
         }
 
-        void onServiceFound(int listenerKey, NsdServiceInfo info) {
+        void onServiceFound(int listenerKey, NsdServiceInfo info, ClientRequest request) {
+            request.onServiceFound(info.getServiceName());
             try {
                 mCb.onServiceFound(listenerKey, info);
             } catch (RemoteException e) {
@@ -2420,7 +2472,8 @@
             }
         }
 
-        void onServiceLost(int listenerKey, NsdServiceInfo info) {
+        void onServiceLost(int listenerKey, NsdServiceInfo info, ClientRequest request) {
+            request.onServiceLost();
             try {
                 mCb.onServiceLost(listenerKey, info);
             } catch (RemoteException e) {
@@ -2436,7 +2489,13 @@
             }
         }
 
-        void onStopDiscoverySucceeded(int listenerKey) {
+        void onStopDiscoverySucceeded(int listenerKey, ClientRequest request) {
+            mMetrics.reportServiceDiscoveryStop(
+                    request.mTransactionId,
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getFoundServiceCount(),
+                    request.getLostServiceCount(),
+                    request.getServicesCount());
             try {
                 mCb.onStopDiscoverySucceeded(listenerKey);
             } catch (RemoteException e) {
@@ -2445,7 +2504,7 @@
         }
 
         void onRegisterServiceFailedImmediately(int listenerKey, int error) {
-            onRegisterServiceFailed(listenerKey, error, NO_TRANSACTION, 0 /* durationMs */);
+            onRegisterServiceFailed(listenerKey, error, NO_TRANSACTION, 0L /* durationMs */);
         }
 
         void onRegisterServiceFailed(int listenerKey, int error, int transactionId,
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index 961c422..fdc12c9 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -94,4 +94,70 @@
             assertEquals(durationMs, it.eventDurationMillisec)
         }
     }
+
+    @Test
+    fun testReportServiceDiscoveryStarted() {
+        val clientId = 99
+        val transactionId = 100
+        val metrics = NetworkNsdReportedMetrics(true /* isLegacy */, clientId, deps)
+        metrics.reportServiceDiscoveryStarted(transactionId)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STARTED, it.queryResult)
+        }
+    }
+
+    @Test
+    fun testReportServiceDiscoveryFailed() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val metrics = NetworkNsdReportedMetrics(false /* isLegacy */, clientId, deps)
+        metrics.reportServiceDiscoveryFailed(transactionId, durationMs)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertFalse(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_FAILED, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
+
+    @Test
+    fun testReportServiceDiscoveryStop() {
+        val clientId = 99
+        val transactionId = 100
+        val durationMs = 10L
+        val foundCallbackCount = 100
+        val lostCallbackCount = 49
+        val servicesCount = 75
+        val metrics = NetworkNsdReportedMetrics(true /* isLegacy */, clientId, deps)
+        metrics.reportServiceDiscoveryStop(
+                transactionId, durationMs, foundCallbackCount, lostCallbackCount, servicesCount)
+
+        val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
+        verify(deps).statsWrite(eventCaptor.capture())
+        eventCaptor.value.let {
+            assertTrue(it.isLegacy)
+            assertEquals(clientId, it.clientId)
+            assertEquals(transactionId, it.transactionId)
+            assertEquals(NsdEventType.NET_DISCOVER, it.type)
+            assertEquals(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP, it.queryResult)
+            assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(foundCallbackCount, it.foundCallbackCount)
+            assertEquals(lostCallbackCount, it.lostCallbackCount)
+            assertEquals(servicesCount, it.foundServiceCount)
+            assertEquals(durationMs, it.eventDurationMillisec)
+        }
+    }
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index f778075..8ed065f 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -31,6 +31,8 @@
 import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
 
 import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
+import static com.android.server.NsdService.MdnsListener;
+import static com.android.server.NsdService.NO_TRANSACTION;
 import static com.android.server.NsdService.parseTypeAndSubtype;
 import static com.android.testutils.ContextUtils.mockService;
 
@@ -45,7 +47,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.any;
@@ -401,9 +402,11 @@
         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
         // this needs to use a timeout
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(discId);
 
         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_FOUND,
                 SERVICE_NAME,
                 SERVICE_TYPE,
@@ -491,9 +494,11 @@
         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
         // this needs to use a timeout
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(discId);
 
         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_FOUND,
                 SERVICE_NAME,
                 SERVICE_TYPE,
@@ -570,19 +575,23 @@
         final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE), eq(IFACE_IDX_ANY));
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+        final int discId = discIdCaptor.getValue();
+        verify(mMetrics).reportServiceDiscoveryStarted(discId);
 
         // Fail to discover service.
         final DiscoveryInfo discoveryFailedInfo = new DiscoveryInfo(
-                discIdCaptor.getValue(),
+                discId,
                 IMDnsEventListener.SERVICE_DISCOVERY_FAILED,
                 null /* serviceName */,
                 null /* registrationType */,
                 null /* domainName */,
                 IFACE_IDX_ANY,
                 0 /* netId */);
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         eventListener.onServiceDiscoveryStatus(discoveryFailedInfo);
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(SERVICE_TYPE, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics).reportServiceDiscoveryFailed(discId, 10L /* durationMs */);
     }
 
     @Test
@@ -936,8 +945,8 @@
         final Network network = new Network(999);
         final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         // Verify the discovery start / stop.
-        final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
-                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final ArgumentCaptor<MdnsListener> listenerCaptor =
+                ArgumentCaptor.forClass(MdnsListener.class);
         client.discoverServices(SERVICE_TYPE, PROTOCOL, network, r -> r.run(), discListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
@@ -945,7 +954,10 @@
                 listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
 
-        final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
+        final MdnsListener listener = listenerCaptor.getValue();
+        final int discId = listener.mTransactionId;
+        verify(mMetrics).reportServiceDiscoveryStarted(discId);
+
         final MdnsServiceInfo foundInfo = new MdnsServiceInfo(
                 SERVICE_NAME, /* serviceInstanceName */
                 serviceTypeWithLocalDomain.split("\\."), /* serviceType */
@@ -987,11 +999,14 @@
                         && info.getServiceType().equals(SERVICE_TYPE + ".")
                         && info.getNetwork().equals(network)));
 
+        doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceDiscovery(discListener);
         waitForIdle();
         verify(mDiscoveryManager).unregisterListener(eq(serviceTypeWithLocalDomain), any());
         verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStopped(SERVICE_TYPE);
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
+        verify(mMetrics).reportServiceDiscoveryStop(discId, 10L /* durationMs */,
+                1 /* foundCallbackCount */, 1 /* lostCallbackCount */, 1 /* servicesCount */);
     }
 
     @Test
@@ -1007,6 +1022,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(invalidServiceType, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(1))
+                .reportServiceDiscoveryFailed(NO_TRANSACTION, 0L /* durationMs */);
 
         final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
         client.discoverServices(
@@ -1014,6 +1031,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(serviceTypeWithLocalDomain, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(2))
+                .reportServiceDiscoveryFailed(NO_TRANSACTION, 0L /* durationMs */);
 
         final String serviceTypeWithoutTcpOrUdpEnding = "_test._com";
         client.discoverServices(
@@ -1021,6 +1040,8 @@
         waitForIdle();
         verify(discListener, timeout(TIMEOUT_MS))
                 .onStartDiscoveryFailed(serviceTypeWithoutTcpOrUdpEnding, FAILURE_INTERNAL_ERROR);
+        verify(mMetrics, times(3))
+                .reportServiceDiscoveryFailed(NO_TRANSACTION, 0L /* durationMs */);
     }
 
     @Test
@@ -1271,7 +1292,7 @@
 
         verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
                 argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
-        verify(mMetrics).reportServiceRegistrationFailed(anyInt(), anyLong());
+        verify(mMetrics).reportServiceRegistrationFailed(NO_TRANSACTION, 0L /* durationMs */);
     }
 
     @Test