Merge "Enable java bpf map always in U devices"
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index fbb342d..846abcb 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -295,8 +295,7 @@
                 NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
         if (h1 == null) return false;
 
-        sendIpv4NfGenMsg(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
-                           (short) (NLM_F_REQUEST | NLM_F_DUMP));
+        requestSocketDump(h1);
 
         final NativeHandle h2 = mDeps.createConntrackSocket(
                 NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY);
@@ -325,7 +324,7 @@
     }
 
     @VisibleForTesting
-    public void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) {
+    void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) {
         final int length = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
         final byte[] msg = new byte[length];
         final ByteBuffer byteBuffer = ByteBuffer.wrap(msg);
@@ -350,6 +349,12 @@
         }
     }
 
+    @VisibleForTesting
+    void requestSocketDump(NativeHandle handle) {
+        sendIpv4NfGenMsg(handle, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
+                (short) (NLM_F_REQUEST | NLM_F_DUMP));
+    }
+
     private void closeFdInNativeHandle(final NativeHandle h) {
         try {
             h.close();
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index d38a7c3..23fb60c 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -16,28 +16,32 @@
 
 package com.android.networkstack.tethering;
 
+import static android.system.OsConstants.EAGAIN;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.NETLINK_NETFILTER;
+
 import static com.android.net.module.util.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE;
-import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
-import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
-import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_GET;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_NEW;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.NFNL_SUBSYS_CTNETLINK;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW;
 
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.NativeHandle;
-import android.system.Os;
+import android.system.ErrnoException;
+import android.util.Log;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.ConntrackMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
 import org.junit.Before;
@@ -45,18 +49,18 @@
 import org.junit.runner.RunWith;
 import org.mockito.MockitoAnnotations;
 
+import java.io.FileDescriptor;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
-import java.net.SocketAddress;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ConntrackSocketTest {
     private static final long TIMEOUT = 500;
+    private static final String TAG = ConntrackSocketTest.class.getSimpleName();
 
     private HandlerThread mHandlerThread;
     private Handler mHandler;
@@ -80,51 +84,72 @@
         mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
     }
 
+    void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
+            throws Exception {
+        Log.d(TAG, "Looking for socket " + local + " -> " + remote);
+
+        // Loop until the socket is found (and return) or recvMessage throws an exception.
+        while (true) {
+            final ByteBuffer buffer = NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT);
+
+            // Parse all the netlink messages in the dump.
+            // NetlinkMessage#parse returns null if the message is truncated or invalid.
+            while (buffer.remaining() > 0) {
+                NetlinkMessage nlmsg = NetlinkMessage.parse(buffer, NETLINK_NETFILTER);
+                Log.d(TAG, "Got netlink message: " + nlmsg);
+                if (!(nlmsg instanceof ConntrackMessage)) {
+                    continue;
+                }
+
+                StructNlMsgHdr nlmsghdr = nlmsg.getHeader();
+                ConntrackMessage ctmsg = (ConntrackMessage) nlmsg;
+                ConntrackMessage.Tuple tuple = ctmsg.tupleOrig;
+
+                if (nlmsghdr.nlmsg_type == (NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_NEW)
+                        && tuple.protoNum == IPPROTO_TCP
+                        && tuple.srcIp.equals(local.getAddress())
+                        && tuple.dstIp.equals(remote.getAddress())
+                        && tuple.srcPort == (short) local.getPort()
+                        && tuple.dstPort == (short) remote.getPort()) {
+                    return;
+                }
+            }
+        }
+    }
+
     @Test
     public void testIpv4ConntrackSocket() throws Exception {
         // Set up server and connect.
-        final InetSocketAddress anyAddress = new InetSocketAddress(
-                InetAddress.getByName("127.0.0.1"), 0);
+        final InetAddress localhost = InetAddress.getByName("127.0.0.1");
+        final InetSocketAddress anyAddress = new InetSocketAddress(localhost, 0);
         final ServerSocket serverSocket = new ServerSocket();
         serverSocket.bind(anyAddress);
-        final SocketAddress theAddress = serverSocket.getLocalSocketAddress();
+        final InetSocketAddress theAddress =
+                (InetSocketAddress) serverSocket.getLocalSocketAddress();
 
         // Make a connection to the server.
         final Socket socket = new Socket();
         socket.connect(theAddress);
+        final InetSocketAddress localAddress = (InetSocketAddress) socket.getLocalSocketAddress();
         final Socket acceptedSocket = serverSocket.accept();
 
         final NativeHandle handle = mDeps.createConntrackSocket(
                 NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
-        mOffloadHw.sendIpv4NfGenMsg(handle,
-                (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
-                (short) (NLM_F_REQUEST | NLM_F_DUMP));
-
-        boolean foundConntrackEntry = false;
-        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_RECV_BUFSIZE);
-        buffer.order(ByteOrder.nativeOrder());
+        mOffloadHw.requestSocketDump(handle);
 
         try {
-            while (Os.read(handle.getFileDescriptor(), buffer) > 0) {
-                buffer.flip();
-
-                // TODO: ConntrackMessage should get a parse API like StructNlMsgHdr
-                // so we can confirm that the conntrack added is for the TCP connection above.
-                final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(buffer);
-                assertNotNull(nlmsghdr);
-
-                // As long as 1 conntrack entry is found test case will pass, even if it's not
-                // the from the TCP connection above.
-                if (nlmsghdr.nlmsg_type == ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW)) {
-                    foundConntrackEntry = true;
-                    break;
-                }
+            findConnectionOrThrow(handle.getFileDescriptor(), localAddress, theAddress);
+            // No exceptions? Socket was found, test passes.
+        } catch (ErrnoException e) {
+            if (e.errno == EAGAIN) {
+                fail("Did not find socket " + localAddress + "->" + theAddress + " in dump");
+            } else {
+                throw e;
             }
         } finally {
             socket.close();
             serverSocket.close();
+            acceptedSocket.close();
         }
-        assertTrue("Did not receive any NFNL_SUBSYS_CTNETLINK/IPCTNL_MSG_CT_NEW message",
-                foundConntrackEntry);
     }
 }
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
index 7a8fcc0..7c19359 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -28,21 +28,24 @@
 public interface MdnsServiceBrowserListener {
 
     /**
-     * Called when an mDNS service instance is found.
+     * Called when an mDNS service instance is found. This method would be called only if all
+     * service records (PTR, SRV, TXT, A or AAAA) are received .
      *
      * @param serviceInfo The found mDNS service instance.
      */
     void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
 
     /**
-     * Called when an mDNS service instance is updated.
+     * Called when an mDNS service instance is updated. This method would be called only if all
+     * service records (PTR, SRV, TXT, A or AAAA) are received before.
      *
      * @param serviceInfo The updated mDNS service instance.
      */
     void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
 
     /**
-     * Called when an mDNS service instance is no longer valid and removed.
+     * Called when a mDNS service instance is no longer valid and removed. This method would be
+     * called only if all service records (PTR, SRV, TXT, A or AAAA) are received before.
      *
      * @param serviceInfo The service instance of the removed mDNS service.
      */
@@ -75,4 +78,19 @@
      * @param errorCode            The error code, defined in {@link MdnsResponseErrorCode}.
      */
     void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+
+    /**
+     * Called when a mDNS service instance is discovered. This method would be called if the PTR
+     * record has been received.
+     *
+     * @param serviceInfo The discovered mDNS service instance.
+     */
+    void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when a discovered mDNS service instance is no longer valid and removed.
+     *
+     * @param serviceInfo The service instance of the removed mDNS service.
+     */
+    void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo);
 }
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index f1b2def..9683bc9 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -270,7 +270,8 @@
     public Map<String, String> getAttributes() {
         Map<String, String> map = new HashMap<>(attributes.size());
         for (Map.Entry<String, byte[]> kv : attributes.entrySet()) {
-            map.put(kv.getKey(), new String(kv.getValue(), UTF_8));
+            final byte[] value = kv.getValue();
+            map.put(kv.getKey(), value == null ? null : new String(value, UTF_8));
         }
         return Collections.unmodifiableMap(map);
     }
@@ -342,7 +343,7 @@
             // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
             // boolean attribute, simply identified as being present, with no value.
             if (delimitPos < 0) {
-                return new TextEntry(new String(textBytes, US_ASCII), "");
+                return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
             } else if (delimitPos == 0) {
                 return null;
             }
@@ -353,13 +354,13 @@
 
         /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
         public TextEntry(String key, String value) {
-            this(key, value.getBytes(UTF_8));
+            this(key, value == null ? null : value.getBytes(UTF_8));
         }
 
         /** Creates a new {@link TextEntry} with given key and value of a byte array. */
         public TextEntry(String key, byte[] value) {
             this.key = key;
-            this.value = value.clone();
+            this.value = value == null ? null : value.clone();
         }
 
         private TextEntry(Parcel in) {
@@ -372,17 +373,24 @@
         }
 
         public byte[] getValue() {
-            return value.clone();
+            return value == null ? null : value.clone();
         }
 
         /** Converts this {@link TextEntry} instance to '=' separated byte array. */
         public byte[] toBytes() {
-            return ByteUtils.concat(key.getBytes(US_ASCII), new byte[]{'='}, value);
+            final byte[] keyBytes = key.getBytes(US_ASCII);
+            if (value == null) {
+                return keyBytes;
+            }
+            return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
         }
 
         /** Converts this {@link TextEntry} instance to '=' separated string. */
         @Override
         public String toString() {
+            if (value == null) {
+                return key;
+            }
             return key + "=" + new String(value, UTF_8);
         }
 
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 0fd6025..dd4ff9b 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -148,10 +148,11 @@
             this.searchOptions = searchOptions;
             if (listeners.add(listener)) {
                 for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+                    final MdnsServiceInfo info =
+                            buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+                    listener.onServiceNameDiscovered(info);
                     if (existingResponse.isComplete()) {
-                        listener.onServiceFound(
-                                buildMdnsServiceInfoFromResponse(existingResponse,
-                                        serviceTypeLabels));
+                        listener.onServiceFound(info);
                     }
                 }
             }
@@ -226,6 +227,7 @@
 
         boolean newServiceFound = false;
         boolean existingServiceChanged = false;
+        boolean serviceBecomesComplete = false;
         if (currentResponse == null) {
             newServiceFound = true;
             currentResponse = response;
@@ -233,10 +235,13 @@
             if (serviceInstanceName != null) {
                 instanceNameToResponse.put(serviceInstanceName, currentResponse);
             }
-        } else if (currentResponse.mergeRecordsFrom(response)) {
-            existingServiceChanged = true;
+        } else {
+            boolean before = currentResponse.isComplete();
+            existingServiceChanged = currentResponse.mergeRecordsFrom(response);
+            boolean after = currentResponse.isComplete();
+            serviceBecomesComplete = !before && after;
         }
-        if (!currentResponse.isComplete() || (!newServiceFound && !existingServiceChanged)) {
+        if (!newServiceFound && !existingServiceChanged) {
             return;
         }
         MdnsServiceInfo serviceInfo =
@@ -244,9 +249,15 @@
 
         for (MdnsServiceBrowserListener listener : listeners) {
             if (newServiceFound) {
-                listener.onServiceFound(serviceInfo);
-            } else {
-                listener.onServiceUpdated(serviceInfo);
+                listener.onServiceNameDiscovered(serviceInfo);
+            }
+
+            if (currentResponse.isComplete()) {
+                if (newServiceFound || serviceBecomesComplete) {
+                    listener.onServiceFound(serviceInfo);
+                } else {
+                    listener.onServiceUpdated(serviceInfo);
+                }
             }
         }
     }
@@ -259,7 +270,10 @@
         for (MdnsServiceBrowserListener listener : listeners) {
             final MdnsServiceInfo serviceInfo =
                     buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
-            listener.onServiceRemoved(serviceInfo);
+            if (response.isComplete()) {
+                listener.onServiceRemoved(serviceInfo);
+            }
+            listener.onServiceNameRemoved(serviceInfo);
         }
     }
 
@@ -423,7 +437,7 @@
                     Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
                     while (iter.hasNext()) {
                         MdnsResponse existingResponse = iter.next();
-                        if (existingResponse.isComplete()
+                        if (existingResponse.hasServiceRecord()
                                 && existingResponse
                                 .getServiceRecord()
                                 .getRemainingTTL(SystemClock.elapsedRealtime())
@@ -436,7 +450,10 @@
                                     final MdnsServiceInfo serviceInfo =
                                             buildMdnsServiceInfoFromResponse(
                                                     existingResponse, serviceTypeLabels);
-                                    listener.onServiceRemoved(serviceInfo);
+                                    if (existingResponse.isComplete()) {
+                                        listener.onServiceRemoved(serviceInfo);
+                                    }
+                                    listener.onServiceNameRemoved(serviceInfo);
                                 }
                             }
                         }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 437622b..8ed735a 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -66,7 +66,6 @@
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
-        "java/com/android/server/NetworkManagementServiceTest.java",
         "java/com/android/server/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index c327868..45a9dbc 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -498,7 +498,7 @@
     public void testConnectivityManagerDoesNotLeakContext() throws Exception {
         final WeakReference<Context> ref = makeConnectivityManagerAndReturnContext();
 
-        final int attempts = 100;
+        final int attempts = 600;
         final long waitIntervalMs = 50;
         for (int i = 0; i < attempts; i++) {
             forceGC();
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
deleted file mode 100644
index 7688a6b..0000000
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * Copyright (C) 2012 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;
-
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
-import static android.util.DebugUtils.valueToString;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.INetdUnsolicitedEventListener;
-import android.net.LinkAddress;
-import android.net.NetworkPolicyManager;
-import android.os.BatteryStats;
-import android.os.Binder;
-import android.os.Build;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.ArrayMap;
-
-import com.android.internal.app.IBatteryStats;
-import com.android.server.NetworkManagementService.Dependencies;
-import com.android.server.net.BaseNetworkObserver;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.function.BiFunction;
-
-/**
- * Tests for {@link NetworkManagementService}.
- */
-@RunWith(DevSdkIgnoreRunner.class)
-@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class NetworkManagementServiceTest {
-    private NetworkManagementService mNMService;
-    @Mock private Context mContext;
-    @Mock private ConnectivityManager mCm;
-    @Mock private IBatteryStats.Stub mBatteryStatsService;
-    @Mock private INetd.Stub mNetdService;
-
-    private static final int TEST_UID = 111;
-
-    @NonNull
-    @Captor
-    private ArgumentCaptor<INetdUnsolicitedEventListener> mUnsolListenerCaptor;
-
-    private final MockDependencies mDeps = new MockDependencies();
-
-    private final class MockDependencies extends Dependencies {
-        @Override
-        public IBinder getService(String name) {
-            switch (name) {
-                case BatteryStats.SERVICE_NAME:
-                    return mBatteryStatsService;
-                default:
-                    throw new UnsupportedOperationException("Unknown service " + name);
-            }
-        }
-
-        @Override
-        public void registerLocalService(NetworkManagementInternal nmi) {
-        }
-
-        @Override
-        public INetd getNetd() {
-            return mNetdService;
-        }
-
-        @Override
-        public int getCallingUid() {
-            return Process.SYSTEM_UID;
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        doNothing().when(mNetdService)
-                .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
-        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
-                eq(ConnectivityManager.class));
-        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
-        // Start the service and wait until it connects to our socket.
-        mNMService = NetworkManagementService.create(mContext, mDeps);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mNMService.shutdown();
-    }
-
-    private static <T> T expectSoon(T mock) {
-        return verify(mock, timeout(200));
-    }
-
-    /**
-     * Tests that network observers work properly.
-     */
-    @Test
-    public void testNetworkObservers() throws Exception {
-        BaseNetworkObserver observer = mock(BaseNetworkObserver.class);
-        doReturn(new Binder()).when(observer).asBinder();  // Used by registerObserver.
-        mNMService.registerObserver(observer);
-
-        // Forget everything that happened to the mock so far, so we can explicitly verify
-        // everything that happens and does not happen to it from now on.
-
-        INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue();
-        reset(observer);
-        // Now call unsolListener methods and ensure that the observer methods are
-        // called. After every method we expect a callback soon after; to ensure that
-        // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end.
-
-        /**
-         * Interface changes.
-         */
-        unsolListener.onInterfaceAdded("rmnet12");
-        expectSoon(observer).interfaceAdded("rmnet12");
-
-        unsolListener.onInterfaceRemoved("eth1");
-        expectSoon(observer).interfaceRemoved("eth1");
-
-        unsolListener.onInterfaceChanged("clat4", true);
-        expectSoon(observer).interfaceStatusChanged("clat4", true);
-
-        unsolListener.onInterfaceLinkStateChanged("rmnet0", false);
-        expectSoon(observer).interfaceLinkStateChanged("rmnet0", false);
-
-        /**
-         * Bandwidth control events.
-         */
-        unsolListener.onQuotaLimitReached("data", "rmnet_usb0");
-        expectSoon(observer).limitReached("data", "rmnet_usb0");
-
-        /**
-         * Interface class activity.
-         */
-        unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID);
-        expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID);
-
-        unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID);
-        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID);
-
-        unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID);
-        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID);
-
-        /**
-         * IP address changes.
-         */
-        unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253);
-        expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253));
-
-        unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253);
-        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253));
-
-        unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0);
-        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0));
-
-        /**
-         * DNS information broadcasts.
-         */
-        unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"});
-        expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600,
-                new String[]{"2001:db8::1"});
-
-        unsolListener.onInterfaceDnsServerInfo("wlan0", 14400,
-                new String[]{"2001:db8::1", "2001:db8::2"});
-        expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400,
-                new String[]{"2001:db8::1", "2001:db8::2"});
-
-        // We don't check for negative lifetimes, only for parse errors.
-        unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"});
-        expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600,
-                new String[]{"::1"});
-
-        // No syntax checking on the addresses.
-        unsolListener.onInterfaceDnsServerInfo("wlan0", 600,
-                new String[]{"", "::", "", "foo", "::1"});
-        expectSoon(observer).interfaceDnsServerInfo("wlan0", 600,
-                new String[]{"", "::", "", "foo", "::1"});
-
-        // Make sure nothing else was called.
-        verifyNoMoreInteractions(observer);
-    }
-
-    @Test
-    public void testFirewallEnabled() {
-        mNMService.setFirewallEnabled(true);
-        assertTrue(mNMService.isFirewallEnabled());
-
-        mNMService.setFirewallEnabled(false);
-        assertFalse(mNMService.isFirewallEnabled());
-    }
-
-    @Test
-    public void testNetworkRestrictedDefault() {
-        assertFalse(mNMService.isNetworkRestricted(TEST_UID));
-    }
-
-    @Test
-    public void testMeteredNetworkRestrictions() throws RemoteException {
-        // Make sure the mocked netd method returns true.
-        doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean());
-
-        // Restrict usage of mobile data in background
-        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
-        assertTrue("Should be true since mobile data usage is restricted",
-                mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
-
-        mNMService.setDataSaverModeEnabled(true);
-        verify(mNetdService).bandwidthEnableDataSaver(true);
-
-        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
-        assertTrue("Should be true since data saver is on and the uid is not allowlisted",
-                mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).removeUidFromMeteredNetworkDenyList(TEST_UID);
-
-        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
-        assertFalse("Should be false since data saver is on and the uid is allowlisted",
-                mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).addUidToMeteredNetworkAllowList(TEST_UID);
-
-        // remove uid from allowlist and turn datasaver off again
-        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
-        verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
-        mNMService.setDataSaverModeEnabled(false);
-        verify(mNetdService).bandwidthEnableDataSaver(false);
-        assertFalse("Network should not be restricted when data saver is off",
-                mNMService.isNetworkRestricted(TEST_UID));
-    }
-
-    @Test
-    public void testFirewallChains() {
-        final ArrayMap<Integer, ArrayMap<Integer, Boolean>> expected = new ArrayMap<>();
-        // Dozable chain
-        final ArrayMap<Integer, Boolean> isRestrictedForDozable = new ArrayMap<>();
-        isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
-        isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false);
-        isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true);
-        expected.put(FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable);
-        // Powersaver chain
-        final ArrayMap<Integer, Boolean> isRestrictedForPowerSave = new ArrayMap<>();
-        isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
-        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false);
-        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true);
-        expected.put(FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave);
-        // Standby chain
-        final ArrayMap<Integer, Boolean> isRestrictedForStandby = new ArrayMap<>();
-        isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false);
-        isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
-        isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true);
-        expected.put(FIREWALL_CHAIN_STANDBY, isRestrictedForStandby);
-        // Restricted mode chain
-        final ArrayMap<Integer, Boolean> isRestrictedForRestrictedMode = new ArrayMap<>();
-        isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
-        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false);
-        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true);
-        expected.put(FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode);
-        // Low Power Standby chain
-        final ArrayMap<Integer, Boolean> isRestrictedForLowPowerStandby = new ArrayMap<>();
-        isRestrictedForLowPowerStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
-        isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
-        isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_DENY, true);
-        expected.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, isRestrictedForLowPowerStandby);
-
-        final int[] chains = {
-                FIREWALL_CHAIN_STANDBY,
-                FIREWALL_CHAIN_POWERSAVE,
-                FIREWALL_CHAIN_DOZABLE,
-                FIREWALL_CHAIN_RESTRICTED,
-                FIREWALL_CHAIN_LOW_POWER_STANDBY
-        };
-        final int[] states = {
-                INetd.FIREWALL_RULE_ALLOW,
-                INetd.FIREWALL_RULE_DENY,
-                NetworkPolicyManager.FIREWALL_RULE_DEFAULT
-        };
-        BiFunction<Integer, Integer, String> errorMsg = (chain, state) -> {
-            return String.format("Unexpected value for chain: %s and state: %s",
-                    valueToString(INetd.class, "FIREWALL_CHAIN_", chain),
-                    valueToString(INetd.class, "FIREWALL_RULE_", state));
-        };
-        for (int chain : chains) {
-            final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
-            mNMService.setFirewallChainEnabled(chain, true);
-            verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
-            for (int state : states) {
-                mNMService.setFirewallUidRule(chain, TEST_UID, state);
-                assertEquals(errorMsg.apply(chain, state),
-                        expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
-            }
-            mNMService.setFirewallChainEnabled(chain, false);
-            verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
-        }
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index d3934c2..ebdb73f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -164,7 +164,8 @@
                         List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
                         List.of(
                                 MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
+                                MdnsServiceInfo.TextEntry.fromString("test=")));
 
         beforeParcel.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -208,11 +209,11 @@
     }
 
     @Test
-    public void textEntry_fromStringWithoutAssignPunc_valueisEmpty() {
+    public void textEntry_fromStringWithoutAssignPunc_noValue() {
         TextEntry entry = TextEntry.fromString("AA");
 
         assertEquals("AA", entry.getKey());
-        assertArrayEquals(new byte[] {}, entry.getValue());
+        assertNull(entry.getValue());
     }
 
     @Test
@@ -241,11 +242,11 @@
     }
 
     @Test
-    public void textEntry_fromBytesWithoutAssignPunc_valueisEmpty() {
+    public void textEntry_fromBytesWithoutAssignPunc_noValue() {
         TextEntry entry = TextEntry.fromBytes(new byte[] {'A', 'A'});
 
         assertEquals("AA", entry.getKey());
-        assertArrayEquals(new byte[] {}, entry.getValue());
+        assertNull(entry.getValue());
     }
 
     @Test
@@ -270,4 +271,12 @@
         assertEquals(new TextEntry("BB", "xxyyzz"), new TextEntry("BB", "xxyyzz"));
         assertEquals(new TextEntry("AA", "XXYYZZ"), new TextEntry("AA", "XXYYZZ"));
     }
+
+    @Test
+    public void textEntry_fromString_valueIsEmpty() {
+        TextEntry entry = TextEntry.fromString("AA=");
+
+        assertEquals("AA", entry.getKey());
+        assertArrayEquals(new byte[] {}, entry.getValue());
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 6f8b85a..462685a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -18,6 +18,7 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -26,16 +27,18 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
 import android.text.TextUtils;
 
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
@@ -49,6 +52,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
@@ -72,7 +76,7 @@
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class MdnsServiceTypeClientTests {
-
+    private static final int INTERFACE_INDEX = 999;
     private static final String SERVICE_TYPE = "_googlecast._tcp.local";
     private static final String[] SERVICE_TYPE_LABELS = TextUtils.split(SERVICE_TYPE, "\\.");
 
@@ -379,15 +383,41 @@
         assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
     }
 
+    private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
+            String[] serviceType, String ipv4Address, String ipv6Address, int port,
+            List<String> subTypes, Map<String, String> attributes, int interfaceIndex) {
+        assertEquals(serviceName, serviceInfo.getServiceInstanceName());
+        assertArrayEquals(serviceType, serviceInfo.getServiceType());
+        assertEquals(ipv4Address, serviceInfo.getIpv4Address());
+        assertEquals(ipv6Address, serviceInfo.getIpv6Address());
+        assertEquals(port, serviceInfo.getPort());
+        assertEquals(subTypes, serviceInfo.getSubtypes());
+        for (String key : attributes.keySet()) {
+            assertEquals(attributes.get(key), serviceInfo.getAttributeByKey(key));
+        }
+        assertEquals(interfaceIndex, serviceInfo.getInterfaceIndex());
+    }
+
     @Test
     public void processResponse_incompleteResponse() {
         client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
         MdnsResponse response = mock(MdnsResponse.class);
         when(response.getServiceInstanceName()).thenReturn("service-instance-1");
+        doReturn(INTERFACE_INDEX).when(response).getInterfaceIndex();
         when(response.isComplete()).thenReturn(false);
 
         client.processResponse(response);
+        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                "service-instance-1",
+                SERVICE_TYPE_LABELS,
+                null /* ipv4Address */,
+                null /* ipv6Address */,
+                0 /* port */,
+                List.of() /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                INTERFACE_INDEX);
 
         verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
         verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
@@ -404,7 +434,7 @@
                         "service-instance-1",
                         ipV4Address,
                         5353,
-                        Collections.singletonList("ABCDE"),
+                        /* subtype= */ "ABCDE",
                         Collections.emptyMap(),
                         /* interfaceIndex= */ 20);
         client.processResponse(initialResponse);
@@ -415,14 +445,26 @@
                         "service-instance-1",
                         ipV4Address,
                         5354,
-                        Collections.singletonList("ABCDE"),
+                        /* subtype= */ "ABCDE",
                         Collections.singletonMap("key", "value"),
                         /* interfaceIndex= */ 20);
         client.processResponse(secondResponse);
 
+        // Verify onServiceNameDiscovered was called once for the initial response.
+        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                "service-instance-1",
+                SERVICE_TYPE_LABELS,
+                ipV4Address /* ipv4Address */,
+                null /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                20 /* interfaceIndex */);
+
         // Verify onServiceFound was called once for the initial response.
         verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
-        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
         assertEquals(initialServiceInfo.getPort(), 5353);
@@ -432,7 +474,7 @@
 
         // Verify onServiceUpdated was called once for the second response.
         verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
-        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(2);
         assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
         assertEquals(updatedServiceInfo.getPort(), 5354);
@@ -453,7 +495,7 @@
                         "service-instance-1",
                         ipV6Address,
                         5353,
-                        Collections.singletonList("ABCDE"),
+                        /* subtype= */ "ABCDE",
                         Collections.emptyMap(),
                         /* interfaceIndex= */ 20);
         client.processResponse(initialResponse);
@@ -464,7 +506,7 @@
                         "service-instance-1",
                         ipV6Address,
                         5354,
-                        Collections.singletonList("ABCDE"),
+                        /* subtype= */ "ABCDE",
                         Collections.singletonMap("key", "value"),
                         /* interfaceIndex= */ 20);
         client.processResponse(secondResponse);
@@ -472,9 +514,21 @@
         System.out.println("secondResponses ip"
                 + secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
 
+        // Verify onServiceNameDiscovered was called once for the initial response.
+        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                "service-instance-1",
+                SERVICE_TYPE_LABELS,
+                null /* ipv4Address */,
+                ipV6Address /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                20 /* interfaceIndex */);
+
         // Verify onServiceFound was called once for the initial response.
         verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
-        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
         assertEquals(initialServiceInfo.getPort(), 5353);
@@ -484,7 +538,7 @@
 
         // Verify onServiceUpdated was called once for the second response.
         verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
-        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(2);
         assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
         assertEquals(updatedServiceInfo.getPort(), 5354);
@@ -494,6 +548,23 @@
         assertEquals(updatedServiceInfo.getInterfaceIndex(), 20);
     }
 
+    private void verifyServiceRemovedNoCallback(MdnsServiceBrowserListener listener) {
+        verify(listener, never()).onServiceRemoved(any());
+        verify(listener, never()).onServiceNameRemoved(any());
+    }
+
+    private void verifyServiceRemovedCallback(MdnsServiceBrowserListener listener,
+            String serviceName, String[] serviceType, int interfaceIndex) {
+        verify(listener).onServiceRemoved(argThat(
+                info -> serviceName.equals(info.getServiceInstanceName())
+                        && Arrays.equals(serviceType, info.getServiceType())
+                        && info.getInterfaceIndex() == interfaceIndex));
+        verify(listener).onServiceNameRemoved(argThat(
+                info -> serviceName.equals(info.getServiceInstanceName())
+                        && Arrays.equals(serviceType, info.getServiceType())
+                        && info.getInterfaceIndex() == interfaceIndex));
+    }
+
     @Test
     public void processResponse_goodBye() throws Exception {
         client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
@@ -501,37 +572,32 @@
 
         final String serviceName = "service-instance-1";
         final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
-        final int interfaceIndex = 999;
         // Process the initial response.
         final MdnsResponse initialResponse =
                 createResponse(
                         serviceName,
                         ipV6Address,
                         5353 /* port */,
-                        Collections.singletonList("ABCDE"),
+                        /* subtype= */ "ABCDE",
                         Collections.emptyMap(),
-                        interfaceIndex);
+                        INTERFACE_INDEX);
         client.processResponse(initialResponse);
         MdnsResponse response = mock(MdnsResponse.class);
         doReturn("goodbye-service").when(response).getServiceInstanceName();
-        doReturn(interfaceIndex).when(response).getInterfaceIndex();
+        doReturn(INTERFACE_INDEX).when(response).getInterfaceIndex();
         doReturn(true).when(response).isGoodbye();
         client.processResponse(response);
-        // Verify onServiceRemoved won't be called if the service is not existed.
-        verify(mockListenerOne, never()).onServiceRemoved(any());
-        verify(mockListenerTwo, never()).onServiceRemoved(any());
+        // Verify removed callback won't be called if the service is not existed.
+        verifyServiceRemovedNoCallback(mockListenerOne);
+        verifyServiceRemovedNoCallback(mockListenerTwo);
 
-        // Verify onServiceRemoved would be called.
+        // Verify removed callback would be called.
         doReturn(serviceName).when(response).getServiceInstanceName();
         client.processResponse(response);
-        verify(mockListenerOne).onServiceRemoved(argThat(
-                info -> serviceName.equals(info.getServiceInstanceName())
-                        && Arrays.equals(SERVICE_TYPE_LABELS, info.getServiceType())
-                        && info.getInterfaceIndex() == interfaceIndex));
-        verify(mockListenerTwo).onServiceRemoved(argThat(
-                info -> serviceName.equals(info.getServiceInstanceName())
-                        && Arrays.equals(SERVICE_TYPE_LABELS, info.getServiceType())
-                        && info.getInterfaceIndex() == interfaceIndex));
+        verifyServiceRemovedCallback(
+                mockListenerOne, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX);
+        verifyServiceRemovedCallback(
+                mockListenerTwo, serviceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX);
     }
 
     @Test
@@ -542,15 +608,28 @@
                         "service-instance-1",
                         "192.168.1.1",
                         5353,
-                        Collections.singletonList("ABCDE"),
-                        Collections.emptyMap());
+                        /* subtype= */ "ABCDE",
+                        Collections.emptyMap(),
+                        INTERFACE_INDEX);
         client.processResponse(initialResponse);
 
         client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
 
+        // Verify onServiceNameDiscovered was called once for the existing response.
+        verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                "service-instance-1",
+                SERVICE_TYPE_LABELS,
+                "192.168.1.1" /* ipv4Address */,
+                null /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                INTERFACE_INDEX);
+
         // Verify onServiceFound was called once for the existing response.
         verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
-        MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(1);
         assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
         assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
         assertEquals(existingServiceInfo.getPort(), 5353);
@@ -567,6 +646,7 @@
 
         // Verify onServiceFound was not called on the newly registered listener after the existing
         // response is gone.
+        verify(mockListenerTwo, never()).onServiceNameDiscovered(any(MdnsServiceInfo.class));
         verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
     }
 
@@ -580,9 +660,9 @@
 
         // Process the initial response.
         MdnsResponse initialResponse =
-                createResponse(
+                createMockResponse(
                         serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
-                        Map.of());
+                        Map.of(), INTERFACE_INDEX);
         client.processResponse(initialResponse);
 
         // Clear the scheduled runnable.
@@ -592,8 +672,8 @@
         when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
         firstMdnsTask.run();
 
-        // Verify onServiceRemoved was not called.
-        verify(mockListenerOne, never()).onServiceRemoved(any());
+        // Verify removed callback was not called.
+        verifyServiceRemovedNoCallback(mockListenerOne);
     }
 
     @Test
@@ -614,9 +694,9 @@
 
         // Process the initial response.
         MdnsResponse initialResponse =
-                createResponse(
+                createMockResponse(
                         serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
-                        Map.of(), 999 /* interfaceIndex */);
+                        Map.of(), INTERFACE_INDEX);
         client.processResponse(initialResponse);
 
         // Clear the scheduled runnable.
@@ -626,18 +706,16 @@
         when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
         firstMdnsTask.run();
 
-        // Verify onServiceRemoved was not called.
-        verify(mockListenerOne, never()).onServiceRemoved(any());
+        // Verify removed callback was not called.
+        verifyServiceRemovedNoCallback(mockListenerOne);
 
         // Simulate the case where the response is after TTL.
         when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
         firstMdnsTask.run();
 
-        // Verify onServiceRemoved was called.
-        verify(mockListenerOne, times(1)).onServiceRemoved(argThat(
-                info -> serviceInstanceName.equals(info.getServiceInstanceName())
-                        && Arrays.equals(SERVICE_TYPE_LABELS, info.getServiceType())
-                        && info.getInterfaceIndex() == 999));
+        // Verify removed callback was called.
+        verifyServiceRemovedCallback(
+                mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX);
     }
 
     @Test
@@ -656,9 +734,9 @@
 
         // Process the initial response.
         MdnsResponse initialResponse =
-                createResponse(
+                createMockResponse(
                         serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
-                        Map.of());
+                        Map.of(), INTERFACE_INDEX);
         client.processResponse(initialResponse);
 
         // Clear the scheduled runnable.
@@ -668,8 +746,8 @@
         when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
         firstMdnsTask.run();
 
-        // Verify onServiceRemoved was not called.
-        verify(mockListenerOne, never()).onServiceRemoved(any());
+        // Verify removed callback was not called.
+        verifyServiceRemovedNoCallback(mockListenerOne);
     }
 
     @Test
@@ -690,9 +768,9 @@
 
         // Process the initial response.
         MdnsResponse initialResponse =
-                createResponse(
+                createMockResponse(
                         serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
-                        Map.of(), 999 /* interfaceIndex */);
+                        Map.of(), INTERFACE_INDEX);
         client.processResponse(initialResponse);
 
         // Clear the scheduled runnable.
@@ -702,11 +780,117 @@
         when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
         firstMdnsTask.run();
 
-        // Verify onServiceRemoved was called.
-        verify(mockListenerOne, times(1)).onServiceRemoved(argThat(
-                info -> serviceInstanceName.equals(info.getServiceInstanceName())
-                        && Arrays.equals(SERVICE_TYPE_LABELS, info.getServiceType())
-                        && info.getInterfaceIndex() == 999));
+        // Verify removed callback was called.
+        verifyServiceRemovedCallback(
+                mockListenerOne, serviceInstanceName, SERVICE_TYPE_LABELS, INTERFACE_INDEX);
+    }
+
+    @Test
+    public void testProcessResponse_InOrder() throws Exception {
+        final String serviceName = "service-instance";
+        final String ipV4Address = "192.0.2.0";
+        final String ipV6Address = "2001:db8::";
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        InOrder inOrder = inOrder(mockListenerOne);
+
+        // Process the initial response which is incomplete.
+        final MdnsResponse initialResponse =
+                createResponse(
+                        serviceName,
+                        null,
+                        5353,
+                        "ABCDE" /* subtype */,
+                        Collections.emptyMap(),
+                        INTERFACE_INDEX);
+        client.processResponse(initialResponse);
+
+        // Process a second response which has ip address to make response become complete.
+        final MdnsResponse secondResponse =
+                createResponse(
+                        serviceName,
+                        ipV4Address,
+                        5353,
+                        "ABCDE" /* subtype */,
+                        Collections.emptyMap(),
+                        INTERFACE_INDEX);
+        client.processResponse(secondResponse);
+
+        // Process a third response with a different ip address, port and updated text attributes.
+        final MdnsResponse thirdResponse =
+                createResponse(
+                        serviceName,
+                        ipV6Address,
+                        5354,
+                        "ABCDE" /* subtype */,
+                        Collections.singletonMap("key", "value"),
+                        INTERFACE_INDEX);
+        client.processResponse(thirdResponse);
+
+        // Process the last response which is goodbye message.
+        final MdnsResponse lastResponse = mock(MdnsResponse.class);
+        doReturn(serviceName).when(lastResponse).getServiceInstanceName();
+        doReturn(true).when(lastResponse).isGoodbye();
+        client.processResponse(lastResponse);
+
+        // Verify onServiceNameDiscovered was first called for the initial response.
+        inOrder.verify(mockListenerOne).onServiceNameDiscovered(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(0),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                null /* ipv4Address */,
+                null /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                INTERFACE_INDEX);
+
+        // Verify onServiceFound was second called for the second response.
+        inOrder.verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(1),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                ipV4Address /* ipv4Address */,
+                null /* ipv6Address */,
+                5353 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", null) /* attributes */,
+                INTERFACE_INDEX);
+
+        // Verify onServiceUpdated was third called for the third response.
+        inOrder.verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(2),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                ipV4Address /* ipv4Address */,
+                ipV6Address /* ipv6Address */,
+                5354 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", "value") /* attributes */,
+                INTERFACE_INDEX);
+
+        // Verify onServiceRemoved was called for the last response.
+        inOrder.verify(mockListenerOne).onServiceRemoved(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(3),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                ipV4Address /* ipv4Address */,
+                ipV6Address /* ipv6Address */,
+                5354 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", "value") /* attributes */,
+                INTERFACE_INDEX);
+
+        // Verify onServiceNameRemoved was called for the last response.
+        inOrder.verify(mockListenerOne).onServiceNameRemoved(serviceInfoCaptor.capture());
+        verifyServiceInfo(serviceInfoCaptor.getAllValues().get(4),
+                serviceName,
+                SERVICE_TYPE_LABELS,
+                ipV4Address /* ipv4Address */,
+                ipV6Address /* ipv6Address */,
+                5354 /* port */,
+                Collections.singletonList("ABCDE") /* subTypes */,
+                Collections.singletonMap("key", "value") /* attributes */,
+                INTERFACE_INDEX);
     }
 
     // verifies that the right query was enqueued with the right delay, and send query by executing
@@ -771,19 +955,8 @@
         }
     }
 
-    private MdnsResponse createResponse(
-            @NonNull String serviceInstanceName,
-            @NonNull String host,
-            int port,
-            @NonNull List<String> subtypes,
-            @NonNull Map<String, String> textAttributes)
-            throws Exception {
-        return createResponse(serviceInstanceName, host, port, subtypes, textAttributes,
-                /* interfaceIndex= */ -1);
-    }
-
-    // Creates a complete mDNS response.
-    private MdnsResponse createResponse(
+    // Creates a mock mDNS response.
+    private MdnsResponse createMockResponse(
             @NonNull String serviceInstanceName,
             @NonNull String host,
             int port,
@@ -830,4 +1003,73 @@
         doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
         return response;
     }
+
+    // Creates a mDNS response.
+    private MdnsResponse createResponse(
+            @NonNull String serviceInstanceName,
+            @Nullable String host,
+            int port,
+            @NonNull String subtype,
+            @NonNull Map<String, String> textAttributes,
+            int interfaceIndex)
+            throws Exception {
+        MdnsResponse response = new MdnsResponse(0);
+        response.setInterfaceIndex(interfaceIndex);
+
+        // Set PTR record
+        final MdnsPointerRecord pointerRecord = new MdnsPointerRecord(
+                new String[]{subtype, MdnsConstants.SUBTYPE_LABEL, "test"} /* name */,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                120000L /* ttlMillis */,
+                new String[]{serviceInstanceName});
+        response.addPointerRecord(pointerRecord);
+
+        // Set SRV record.
+        final MdnsServiceRecord serviceRecord = new MdnsServiceRecord(
+                new String[] {"service"} /* name */,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                120000L /* ttlMillis */,
+                0 /* servicePriority */,
+                0 /* serviceWeight */,
+                port,
+                new String[]{"hostname"});
+        response.setServiceRecord(serviceRecord);
+
+        // Set A/AAAA record.
+        if (host != null) {
+            if (InetAddresses.parseNumericAddress(host) instanceof Inet6Address) {
+                final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord(
+                        new String[] {"address"} /* name */,
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        Inet6Address.getByName(host));
+                response.setInet6AddressRecord(inetAddressRecord);
+            } else {
+                final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord(
+                        new String[] {"address"} /* name */,
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        Inet4Address.getByName(host));
+                response.setInet4AddressRecord(inetAddressRecord);
+            }
+        }
+
+        // Set TXT record.
+        final List<TextEntry> textEntries = new ArrayList<>();
+        for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
+            textEntries.add(new TextEntry(kv.getKey(), kv.getValue().getBytes(UTF_8)));
+        }
+        final MdnsTextRecord textRecord = new MdnsTextRecord(
+                new String[] {"text"} /* name */,
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                120000L /* ttlMillis */,
+                textEntries);
+        response.setTextRecord(textRecord);
+        return response;
+    }
 }
\ No newline at end of file