Merge "Add wtf log for the case that should not happen"
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 6014722..876b945 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -24,7 +24,9 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringTester.TestDnsPacket;
 import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpDnsPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
 import static android.system.OsConstants.IPPROTO_IP;
 import static android.system.OsConstants.IPPROTO_IPV6;
@@ -81,7 +83,9 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DeviceInfoUtils;
@@ -156,6 +160,8 @@
     private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
+    private static final short DNS_PORT = 53;
+
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
     private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -165,6 +171,66 @@
     private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
     private static final short HOP_LIMIT = 0x40;
 
+    // TODO: use class DnsPacket to build DNS query and reply message once DnsPacket supports
+    // building packet for given arguments.
+    private static final ByteBuffer DNS_QUERY = ByteBuffer.wrap(new byte[] {
+            // scapy.DNS(
+            //   id=0xbeef,
+            //   qr=0,
+            //   qd=scapy.DNSQR(qname="hello.example.com"))
+            //
+            /* Header */
+            (byte) 0xbe, (byte) 0xef, /* Transaction ID: 0xbeef */
+            (byte) 0x01, (byte) 0x00, /* Flags: rd */
+            (byte) 0x00, (byte) 0x01, /* Questions: 1 */
+            (byte) 0x00, (byte) 0x00, /* Answer RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
+            /* Queries */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00, /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,              /* Type: A */
+            (byte) 0x00, (byte) 0x01               /* Class: IN */
+    });
+
+    private static final byte[] DNS_REPLY = new byte[] {
+            // scapy.DNS(
+            //   id=0,
+            //   qr=1,
+            //   qd=scapy.DNSQR(qname="hello.example.com"),
+            //   an=scapy.DNSRR(rrname="hello.example.com", rdata='1.2.3.4'))
+            //
+            /* Header */
+            (byte) 0x00, (byte) 0x00, /* Transaction ID: 0x0, must be updated by dns query id */
+            (byte) 0x81, (byte) 0x00, /* Flags: qr rd */
+            (byte) 0x00, (byte) 0x01, /* Questions: 1 */
+            (byte) 0x00, (byte) 0x01, /* Answer RRs: 1 */
+            (byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
+            /* Queries */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00,              /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,                           /* Type: A */
+            (byte) 0x00, (byte) 0x01,                           /* Class: IN */
+            /* Answers */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00,              /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,                           /* Type: A */
+            (byte) 0x00, (byte) 0x01,                           /* Class: IN */
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* Time to live: 0 */
+            (byte) 0x00, (byte) 0x04,                           /* Data length: 4 */
+            (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04  /* Address: 1.2.3.4 */
+    };
+
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
     private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -1371,6 +1437,94 @@
         runClatUdpTest();
     }
 
+    @NonNull
+    private ByteBuffer buildDnsReplyMessageById(short id) {
+        byte[] replyMessage = Arrays.copyOf(DNS_REPLY, DNS_REPLY.length);
+        // Assign transaction id of reply message pattern with a given DNS transaction id.
+        replyMessage[0] = (byte) ((id >> 8) & 0xff);
+        replyMessage[1] = (byte) (id & 0xff);
+        Log.d(TAG, "Built DNS reply: " + dumpHexString(replyMessage));
+
+        return ByteBuffer.wrap(replyMessage);
+    }
+
+    @NonNull
+    private void sendDownloadPacketDnsV4(@NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, short srcPort, short dstPort, short dnsId,
+            @NonNull final TetheringTester tester) throws Exception {
+        // DNS response transaction id must be copied from DNS query. Used by the requester
+        // to match up replies to outstanding queries. See RFC 1035 section 4.1.1.
+        final ByteBuffer dnsReplyMessage = buildDnsReplyMessageById(dnsId);
+        final ByteBuffer testPacket = buildUdpPacket((InetAddress) srcIp,
+                (InetAddress) dstIp, srcPort, dstPort, dnsReplyMessage);
+
+        tester.verifyDownload(testPacket, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedUdpDnsPacket(p, true /* hasEther */, true /* isIpv4 */,
+                    dnsReplyMessage);
+        });
+    }
+
+    // Send IPv4 UDP DNS packet and return the forwarded DNS packet on upstream.
+    @NonNull
+    private byte[] sendUploadPacketDnsV4(@NonNull final MacAddress srcMac,
+            @NonNull final MacAddress dstMac, @NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+            @NonNull final TetheringTester tester) throws Exception {
+        final ByteBuffer testPacket = buildUdpPacket(srcMac, dstMac, srcIp, dstIp,
+                srcPort, dstPort, DNS_QUERY);
+
+        return tester.verifyUpload(testPacket, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+            return isExpectedUdpDnsPacket(p, false /* hasEther */, true /* isIpv4 */,
+                    DNS_QUERY);
+        });
+    }
+
+    @Test
+    public void testTetherUdpV4Dns() throws Exception {
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+                toList(TEST_IP4_DNS));
+        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+        // TODO: remove the connectivity verification for upstream connected notification race.
+        // See the same reason in runUdp4Test().
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
+
+        // [1] Send DNS query.
+        // tethered device --> downstream --> dnsmasq forwarding --> upstream --> DNS server
+        //
+        // Need to extract DNS transaction id and source port from dnsmasq forwarded DNS query
+        // packet. dnsmasq forwarding creats new query which means UDP source port and DNS
+        // transaction id are changed from original sent DNS query. See forward_query() in
+        // external/dnsmasq/src/forward.c. Note that #TetheringTester.isExpectedUdpDnsPacket
+        // guarantees that |forwardedQueryPacket| is a valid DNS packet. So we can parse it as DNS
+        // packet.
+        final MacAddress srcMac = tethered.macAddr;
+        final MacAddress dstMac = tethered.routerMacAddr;
+        final Inet4Address clientIp = tethered.ipv4Addr;
+        final Inet4Address gatewayIp = tethered.ipv4Gatway;
+        final byte[] forwardedQueryPacket = sendUploadPacketDnsV4(srcMac, dstMac, clientIp,
+                gatewayIp, LOCAL_PORT, DNS_PORT, tester);
+        final ByteBuffer buf = ByteBuffer.wrap(forwardedQueryPacket);
+        Struct.parse(Ipv4Header.class, buf);
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
+        final TestDnsPacket dnsQuery = TestDnsPacket.getTestDnsPacket(buf);
+        assertNotNull(dnsQuery);
+        Log.d(TAG, "Forwarded UDP source port: " + udpHeader.srcPort + ", DNS query id: "
+                + dnsQuery.getHeader().id);
+
+        // [2] Send DNS reply.
+        // DNS server --> upstream --> dnsmasq forwarding --> downstream --> tethered device
+        //
+        // DNS reply transaction id must be copied from DNS query. Used by the requester to match
+        // up replies to outstanding queries. See RFC 1035 section 4.1.1.
+        final Inet4Address remoteIp = (Inet4Address) TEST_IP4_DNS;
+        final Inet4Address tetheringUpstreamIp = (Inet4Address) TEST_IP4_ADDR.getAddress();
+        sendDownloadPacketDnsV4(remoteIp, tetheringUpstreamIp, DNS_PORT,
+                (short) udpHeader.srcPort, (short) dnsQuery.getHeader().id, tester);
+    }
+
     private <T> List<T> toList(T... array) {
         return Arrays.asList(array);
     }
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index 4d90d39..48a406c 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -20,6 +20,11 @@
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.DnsPacket.ANSECTION;
+import static com.android.net.module.util.DnsPacket.ARSECTION;
+import static com.android.net.module.util.DnsPacket.NSSECTION;
+import static com.android.net.module.util.DnsPacket.QDSECTION;
+import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
@@ -41,12 +46,14 @@
 import android.net.dhcp.DhcpAckPacket;
 import android.net.dhcp.DhcpOfferPacket;
 import android.net.dhcp.DhcpPacket;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.DnsPacket;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
@@ -124,12 +131,14 @@
         public final MacAddress macAddr;
         public final MacAddress routerMacAddr;
         public final Inet4Address ipv4Addr;
+        public final Inet4Address ipv4Gatway;
         public final Inet6Address ipv6Addr;
 
         private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
             macAddr = mac;
             DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
             ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
+            ipv4Gatway = (Inet4Address) dhcpResults.gateway;
             routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
                     dhcpResults.serverAddress);
             ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
@@ -386,8 +395,8 @@
         }
     }
 
-    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
-            boolean isIpv4, @NonNull final ByteBuffer payload) {
+    private static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, Predicate<ByteBuffer> payloadVerifier) {
         final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
         try {
             if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
@@ -395,15 +404,178 @@
             if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
 
             if (Struct.parse(UdpHeader.class, buf) == null) return false;
+
+            if (!payloadVerifier.test(buf)) return false;
         } catch (Exception e) {
             // Parsing packet fail means it is not udp packet.
             return false;
         }
+        return true;
+    }
 
-        if (buf.remaining() != payload.limit()) return false;
+    // Returns remaining bytes in the ByteBuffer in a new byte array of the right size. The
+    // ByteBuffer will be empty upon return. Used to avoid lint warning.
+    // See https://errorprone.info/bugpattern/ByteBufferBackingArray
+    private static byte[] getRemaining(final ByteBuffer buf) {
+        final byte[] bytes = new byte[buf.remaining()];
+        buf.get(bytes);
+        Log.d(TAG, "Get remaining bytes: " + dumpHexString(bytes));
+        return bytes;
+    }
 
-        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
-                payload.array());
+    // |expectedPayload| is copied as read-only because the caller may reuse it.
+    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
+        return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
+            if (p.remaining() != expectedPayload.limit()) return false;
+
+            return Arrays.equals(getRemaining(p), getRemaining(
+                    expectedPayload.asReadOnlyBuffer()));
+        });
+    }
+
+    // |expectedPayload| is copied as read-only because the caller may reuse it.
+    // See hasExpectedDnsMessage.
+    public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
+        return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
+            return hasExpectedDnsMessage(p, expectedPayload);
+        });
+    }
+
+    public static class TestDnsPacket extends DnsPacket {
+        TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
+            super(data);
+        }
+
+        @Nullable
+        public static TestDnsPacket getTestDnsPacket(final ByteBuffer buf) {
+            try {
+                // The ByteBuffer will be empty upon return.
+                return new TestDnsPacket(getRemaining(buf));
+            } catch (DnsPacket.ParseException e) {
+                return null;
+            }
+        }
+
+        public DnsHeader getHeader() {
+            return mHeader;
+        }
+
+        public List<DnsRecord> getRecordList(int secType) {
+            return mRecords[secType];
+        }
+
+        public int getANCount() {
+            return mHeader.getRecordCount(ANSECTION);
+        }
+
+        public int getQDCount() {
+            return mHeader.getRecordCount(QDSECTION);
+        }
+
+        public int getNSCount() {
+            return mHeader.getRecordCount(NSSECTION);
+        }
+
+        public int getARCount() {
+            return mHeader.getRecordCount(ARSECTION);
+        }
+
+        private boolean isRecordsEquals(int type, @NonNull final TestDnsPacket other) {
+            List<DnsRecord> records = getRecordList(type);
+            List<DnsRecord> otherRecords = other.getRecordList(type);
+
+            if (records.size() != otherRecords.size()) return false;
+
+            // Expect that two compared resource records are in the same order. For current tests
+            // in EthernetTetheringTest, it is okay because dnsmasq doesn't reorder the forwarded
+            // resource records.
+            // TODO: consider allowing that compare records out of order.
+            for (int i = 0; i < records.size(); i++) {
+                // TODO: use DnsRecord.equals once aosp/1387135 is merged.
+                if (!TextUtils.equals(records.get(i).dName, otherRecords.get(i).dName)
+                        || records.get(i).nsType != otherRecords.get(i).nsType
+                        || records.get(i).nsClass != otherRecords.get(i).nsClass
+                        || records.get(i).ttl != otherRecords.get(i).ttl
+                        || !Arrays.equals(records.get(i).getRR(), otherRecords.get(i).getRR())) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public boolean isQDRecordsEquals(@NonNull final TestDnsPacket other) {
+            return isRecordsEquals(QDSECTION, other);
+        }
+
+        public boolean isANRecordsEquals(@NonNull final TestDnsPacket other) {
+            return isRecordsEquals(ANSECTION, other);
+        }
+    }
+
+    // The ByteBuffer |actual| will be empty upon return. The ByteBuffer |excepted| will be copied
+    // as read-only because the caller may reuse it.
+    private static boolean hasExpectedDnsMessage(@NonNull final ByteBuffer actual,
+            @NonNull final ByteBuffer excepted) {
+        // Forwarded DNS message is extracted from remaining received packet buffer which has
+        // already parsed ethernet header, if any, IP header and UDP header.
+        final TestDnsPacket forwardedDns = TestDnsPacket.getTestDnsPacket(actual);
+        if (forwardedDns == null) return false;
+
+        // Original DNS message is the payload of the sending test UDP packet. It is used to check
+        // that the forwarded DNS query and reply have corresponding contents.
+        final TestDnsPacket originalDns = TestDnsPacket.getTestDnsPacket(
+                excepted.asReadOnlyBuffer());
+        assertNotNull(originalDns);
+
+        // Compare original DNS message which is sent to dnsmasq and forwarded DNS message which
+        // is forwarded by dnsmasq. The original message and forwarded message may be not identical
+        // because dnsmasq may change the header flags or even recreate the DNS query message and
+        // so on. We only simple check on forwarded packet and monitor if test will be broken by
+        // vendor dnsmasq customization. See forward_query() in external/dnsmasq/src/forward.c.
+        //
+        // DNS message format. See rfc1035 section 4.1.
+        // +---------------------+
+        // |        Header       |
+        // +---------------------+
+        // |       Question      | the question for the name server
+        // +---------------------+
+        // |        Answer       | RRs answering the question
+        // +---------------------+
+        // |      Authority      | RRs pointing toward an authority
+        // +---------------------+
+        // |      Additional     | RRs holding additional information
+        // +---------------------+
+
+        // [1] Header section. See rfc1035 section 4.1.1.
+        // Verify QR flag bit, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT.
+        if (originalDns.getHeader().isResponse() != forwardedDns.getHeader().isResponse()) {
+            return false;
+        }
+        if (originalDns.getQDCount() != forwardedDns.getQDCount()) return false;
+        if (originalDns.getANCount() != forwardedDns.getANCount()) return false;
+        if (originalDns.getNSCount() != forwardedDns.getNSCount()) return false;
+        if (originalDns.getARCount() != forwardedDns.getARCount()) return false;
+
+        // [2] Question section. See rfc1035 section 4.1.2.
+        // Question section has at least one entry either DNS query or DNS reply.
+        if (forwardedDns.getRecordList(QDSECTION).isEmpty()) return false;
+        // Expect that original and forwarded message have the same question records (usually 1).
+        if (!originalDns.isQDRecordsEquals(forwardedDns)) return false;
+
+        // [3] Answer section. See rfc1035 section 4.1.3.
+        if (forwardedDns.getHeader().isResponse()) {
+            // DNS reply has at least have one answer in our tests.
+            // See EthernetTetheringTest#testTetherUdpV4Dns.
+            if (forwardedDns.getRecordList(ANSECTION).isEmpty()) return false;
+            // Expect that original and forwarded message have the same answer records.
+            if (!originalDns.isANRecordsEquals(forwardedDns)) return false;
+        }
+
+        // Ignore checking {Authority, Additional} sections because they are not tested
+        // in EthernetTetheringTest.
+        return true;
     }
 
     private void sendUploadPacket(ByteBuffer packet) throws Exception {
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 8818460..1226eea 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -295,6 +295,13 @@
                         if (DBG) Log.d(TAG, "Discover services");
                         args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in discovery");
+                            break;
+                        }
 
                         if (requestLimitReached(clientInfo)) {
                             clientInfo.onDiscoverServicesFailed(
@@ -321,6 +328,13 @@
                         if (DBG) Log.d(TAG, "Stop service discovery");
                         args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in stop discovery");
+                            break;
+                        }
 
                         try {
                             id = clientInfo.mClientIds.get(clientId);
@@ -341,6 +355,14 @@
                         if (DBG) Log.d(TAG, "Register service");
                         args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in registration");
+                            break;
+                        }
+
                         if (requestLimitReached(clientInfo)) {
                             clientInfo.onRegisterServiceFailed(
                                     clientId, NsdManager.FAILURE_MAX_LIMIT);
@@ -363,6 +385,9 @@
                         if (DBG) Log.d(TAG, "unregister service");
                         args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
                         if (clientInfo == null) {
                             Log.e(TAG, "Unknown connector in unregistration");
                             break;
@@ -380,6 +405,13 @@
                         if (DBG) Log.d(TAG, "Resolve service");
                         args = (ListenerArgs) msg.obj;
                         clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in resolution");
+                            break;
+                        }
 
                         if (clientInfo.mResolvedService != null) {
                             clientInfo.onResolveServiceFailed(
diff --git a/service-t/src/com/android/server/ethernet/EthernetCallback.java b/service-t/src/com/android/server/ethernet/EthernetCallback.java
new file mode 100644
index 0000000..5461156
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetCallback.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 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.ethernet;
+
+import android.net.EthernetNetworkManagementException;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/** Convenience wrapper for INetworkInterfaceOutcomeReceiver */
+@VisibleForTesting
+public class EthernetCallback {
+    private static final String TAG = EthernetCallback.class.getSimpleName();
+    private final INetworkInterfaceOutcomeReceiver mReceiver;
+
+    public EthernetCallback(INetworkInterfaceOutcomeReceiver receiver) {
+        mReceiver = receiver;
+    }
+
+    /** Calls INetworkInterfaceOutcomeReceiver#onResult */
+    public void onResult(String ifname) {
+        try {
+            if (mReceiver != null) {
+                mReceiver.onResult(ifname);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to report error to OutcomeReceiver", e);
+        }
+    }
+
+    /** Calls INetworkInterfaceOutcomeReceiver#onError */
+    public void onError(String msg) {
+        try {
+            if (mReceiver != null) {
+                mReceiver.onError(new EthernetNetworkManagementException(msg));
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to report error to OutcomeReceiver", e);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index e5bddf6..56c21eb 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -22,9 +22,7 @@
 import android.net.ConnectivityManager;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
-import android.net.EthernetNetworkManagementException;
 import android.net.EthernetNetworkSpecifier;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
@@ -42,7 +40,6 @@
 import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
 import android.util.ArraySet;
@@ -190,22 +187,19 @@
      *                     {@code null} is passed, then the network's current
      *                     {@link NetworkCapabilities} will be used in support of existing APIs as
      *                     the public API does not allow this.
-     * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of
-     *                 completion.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected void updateInterface(@NonNull final String ifaceName,
             @Nullable final IpConfiguration ipConfig,
-            @Nullable final NetworkCapabilities capabilities,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final NetworkCapabilities capabilities) {
         if (!hasInterface(ifaceName)) {
-            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
             return;
         }
 
         final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
-        iface.updateInterface(ipConfig, capabilities, listener);
+        iface.updateInterface(ipConfig, capabilities);
         mTrackingInterfaces.put(ifaceName, iface);
+        return;
     }
 
     private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
@@ -238,10 +232,8 @@
 
     /** Returns true if state has been modified */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up) {
         if (!hasInterface(ifaceName)) {
-            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
             return false;
         }
 
@@ -250,14 +242,7 @@
         }
 
         NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
-        return iface.updateLinkState(up, listener);
-    }
-
-    private void maybeSendNetworkManagementCallbackForUntracked(
-            String ifaceName, INetworkInterfaceOutcomeReceiver listener) {
-        maybeSendNetworkManagementCallback(listener, null,
-                new EthernetNetworkManagementException(
-                        ifaceName + " can't be updated as it is not available."));
+        return iface.updateLinkState(up);
     }
 
     @VisibleForTesting
@@ -265,25 +250,6 @@
         return mTrackingInterfaces.containsKey(ifaceName);
     }
 
-    private static void maybeSendNetworkManagementCallback(
-            @Nullable final INetworkInterfaceOutcomeReceiver listener,
-            @Nullable final String iface,
-            @Nullable final EthernetNetworkManagementException e) {
-        if (null == listener) {
-            return;
-        }
-
-        try {
-            if (iface != null) {
-                listener.onResult(iface);
-            } else {
-                listener.onError(e);
-            }
-        } catch (RemoteException re) {
-            Log.e(TAG, "Can't send onComplete for network management callback", re);
-        }
-    }
-
     @VisibleForTesting
     static class NetworkInterfaceState {
         final String name;
@@ -332,11 +298,6 @@
         private class EthernetIpClientCallback extends IpClientCallbacks {
             private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
             private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
-            @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener;
-
-            EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
-                mNetworkManagementListener = listener;
-            }
 
             @Override
             public void onIpClientCreated(IIpClient ipClient) {
@@ -372,14 +333,14 @@
 
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
-                handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
+                handleIpEvent(() -> onIpLayerStarted(newLp));
             }
 
             @Override
             public void onProvisioningFailure(LinkProperties newLp) {
                 // This cannot happen due to provisioning timeout, because our timeout is 0. It can
                 // happen due to errors while provisioning or on provisioning loss.
-                handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener));
+                handleIpEvent(() -> onIpLayerStopped());
             }
 
             @Override
@@ -491,13 +452,11 @@
         }
 
         void updateInterface(@Nullable final IpConfiguration ipConfig,
-                @Nullable final NetworkCapabilities capabilities,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+                @Nullable final NetworkCapabilities capabilities) {
             if (DBG) {
                 Log.d(TAG, "updateInterface, iface: " + name
                         + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
                         + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
-                        + ", listener: " + listener
                 );
             }
 
@@ -510,7 +469,7 @@
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
-            restart(listener);
+            restart();
         }
 
         boolean isRestricted() {
@@ -518,10 +477,6 @@
         }
 
         private void start() {
-            start(null);
-        }
-
-        private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
             if (mIpClient != null) {
                 if (DBG) Log.d(TAG, "IpClient already started");
                 return;
@@ -530,7 +485,7 @@
                 Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
             }
 
-            mIpClientCallback = new EthernetIpClientCallback(listener);
+            mIpClientCallback = new EthernetIpClientCallback();
             mDeps.makeIpClient(mContext, name, mIpClientCallback);
             mIpClientCallback.awaitIpClientStart();
 
@@ -540,8 +495,7 @@
             provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
         }
 
-        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties) {
             if (mNetworkAgent != null) {
                 Log.e(TAG, "Already have a NetworkAgent - aborting new request");
                 stop();
@@ -573,40 +527,18 @@
                     });
             mNetworkAgent.register();
             mNetworkAgent.markConnected();
-            realizeNetworkManagementCallback(name, null);
         }
 
-        void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        void onIpLayerStopped() {
             // There is no point in continuing if the interface is gone as stop() will be triggered
             // by removeInterface() when processed on the handler thread and start() won't
             // work for a non-existent interface.
             if (null == mDeps.getNetworkInterfaceByName(name)) {
                 if (DBG) Log.d(TAG, name + " is no longer available.");
                 // Send a callback in case a provisioning request was in progress.
-                maybeSendNetworkManagementCallbackForAbort();
                 return;
             }
-            restart(listener);
-        }
-
-        private void maybeSendNetworkManagementCallbackForAbort() {
-            realizeNetworkManagementCallback(null,
-                    new EthernetNetworkManagementException(
-                            "The IP provisioning request has been aborted."));
-        }
-
-        // Must be called on the handler thread
-        private void realizeNetworkManagementCallback(@Nullable final String iface,
-                @Nullable final EthernetNetworkManagementException e) {
-            ensureRunningOnEthernetHandlerThread();
-            if (null == mIpClientCallback) {
-                return;
-            }
-
-            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
-                    mIpClientCallback.mNetworkManagementListener, iface, e);
-            // Only send a single callback per listener.
-            mIpClientCallback.mNetworkManagementListener = null;
+            restart();
         }
 
         private void ensureRunningOnEthernetHandlerThread() {
@@ -636,12 +568,8 @@
         }
 
         /** Returns true if state has been modified */
-        boolean updateLinkState(final boolean up,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        boolean updateLinkState(final boolean up) {
             if (mLinkUp == up)  {
-                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null,
-                        new EthernetNetworkManagementException(
-                                "No changes with requested link state " + up + " for " + name));
                 return false;
             }
             mLinkUp = up;
@@ -654,7 +582,6 @@
                 registerNetworkOffer();
             }
 
-            EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
             return true;
         }
 
@@ -665,8 +592,7 @@
                 mIpClientCallback.awaitIpClientShutdown();
                 mIpClient = null;
             }
-            // Send an abort callback if an updateInterface request was in progress.
-            maybeSendNetworkManagementCallbackForAbort();
+
             mIpClientCallback = null;
 
             if (mNetworkAgent != null) {
@@ -723,13 +649,9 @@
         }
 
         void restart() {
-            restart(null);
-        }
-
-        void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
             if (DBG) Log.d(TAG, "reconnecting Ethernet");
             stop();
-            start(listener);
+            start();
         }
 
         @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index dae3d2a..edf04b2 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -260,7 +260,7 @@
     @Override
     public void updateConfiguration(@NonNull final String iface,
             @NonNull final EthernetNetworkUpdateRequest request,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
         Objects.requireNonNull(iface);
         Objects.requireNonNull(request);
         throwIfEthernetNotStarted();
@@ -277,31 +277,31 @@
         }
 
         mTracker.updateConfiguration(
-                iface, request.getIpConfiguration(), nc, listener);
+                iface, request.getIpConfiguration(), nc, new EthernetCallback(cb));
     }
 
     @Override
     public void enableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
+        Log.i(TAG, "enableInterface called with: iface=" + iface + ", cb=" + cb);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
         enforceAdminPermission(iface, false, "enableInterface()");
 
-        mTracker.enableInterface(iface, listener);
+        mTracker.enableInterface(iface, new EthernetCallback(cb));
     }
 
     @Override
     public void disableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
+        Log.i(TAG, "disableInterface called with: iface=" + iface + ", cb=" + cb);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
         enforceAdminPermission(iface, false, "disableInterface()");
 
-        mTracker.disableInterface(iface, listener);
+        mTracker.disableInterface(iface, new EthernetCallback(cb));
     }
 
     @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index ba367cf..00dff5b 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -29,7 +29,6 @@
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.ITetheredInterfaceCallback;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
@@ -271,7 +270,7 @@
         }
         writeIpConfiguration(iface, ipConfiguration);
         mHandler.post(() -> {
-            mFactory.updateInterface(iface, ipConfiguration, null, null);
+            mFactory.updateInterface(iface, ipConfiguration, null);
             broadcastInterfaceStateChange(iface);
         });
     }
@@ -335,7 +334,7 @@
     protected void updateConfiguration(@NonNull final String iface,
             @Nullable final IpConfiguration ipConfig,
             @Nullable final NetworkCapabilities capabilities,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final EthernetCallback cb) {
         if (DBG) {
             Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
                     + ", ipConfig: " + ipConfig);
@@ -353,21 +352,29 @@
             mNetworkCapabilities.put(iface, capabilities);
         }
         mHandler.post(() -> {
-            mFactory.updateInterface(iface, localIpConfig, capabilities, listener);
-            broadcastInterfaceStateChange(iface);
+            mFactory.updateInterface(iface, localIpConfig, capabilities);
+
+            // only broadcast state change when the ip configuration is updated.
+            if (ipConfig != null) {
+                broadcastInterfaceStateChange(iface);
+            }
+            // Always return success. Even if the interface does not currently exist, the
+            // IpConfiguration and NetworkCapabilities were saved and will be applied if an
+            // interface with the given name is ever added.
+            cb.onResult(iface);
         });
     }
 
     @VisibleForTesting(visibility = PACKAGE)
     protected void enableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+            @Nullable final EthernetCallback cb) {
+        mHandler.post(() -> updateInterfaceState(iface, true, cb));
     }
 
     @VisibleForTesting(visibility = PACKAGE)
     protected void disableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        mHandler.post(() -> updateInterfaceState(iface, false, listener));
+            @Nullable final EthernetCallback cb) {
+        mHandler.post(() -> updateInterfaceState(iface, false, cb));
     }
 
     IpConfiguration getIpConfiguration(String iface) {
@@ -571,8 +578,14 @@
         // Bring up the interface so we get link status indications.
         try {
             PermissionUtils.enforceNetworkStackPermission(mContext);
-            NetdUtils.setInterfaceUp(mNetd, iface);
+            // Read the flags before attempting to bring up the interface. If the interface is
+            // already running an UP event is created after adding the interface.
             config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
+            if (NetdUtils.hasFlag(config, INetd.IF_STATE_DOWN)) {
+                // As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
+                // address and readds it which *could* lead to unexpected behavior in the future.
+                NetdUtils.setInterfaceUp(mNetd, iface);
+            }
         } catch (IllegalStateException e) {
             // Either the system is crashing or the interface has disappeared. Just ignore the
             // error; we haven't modified any state because we only do that if our calls succeed.
@@ -608,23 +621,31 @@
         // Note: if the interface already has link (e.g., if we crashed and got
         // restarted while it was running), we need to fake a link up notification so we
         // start configuring it.
-        if (NetdUtils.hasFlag(config, "running")) {
+        if (NetdUtils.hasFlag(config, INetd.IF_FLAG_RUNNING)) {
             updateInterfaceState(iface, true);
         }
     }
 
     private void updateInterfaceState(String iface, boolean up) {
-        updateInterfaceState(iface, up, null /* listener */);
+        // TODO: pull EthernetCallbacks out of EthernetNetworkFactory.
+        updateInterfaceState(iface, up, new EthernetCallback(null /* cb */));
     }
 
     private void updateInterfaceState(@NonNull final String iface, final boolean up,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final EthernetCallback cb) {
         final int mode = getInterfaceMode(iface);
         final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT)
-                && mFactory.updateInterfaceLinkState(iface, up, listener);
+                && mFactory.updateInterfaceLinkState(iface, up);
 
         if (factoryLinkStateUpdated) {
             broadcastInterfaceStateChange(iface);
+            cb.onResult(iface);
+        } else {
+            // The interface may already be in the correct state. While usually this should not be
+            // an error, since updateInterfaceState is used in setInterfaceEnabled() /
+            // setInterfaceDisabled() it has to be reported as such.
+            // It is also possible that the interface disappeared or is in server mode.
+            cb.onError("Failed to set link state " + (up ? "up" : "down") + " for " + iface);
         }
     }
 
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 839cd16..6bb8115 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -40,6 +40,7 @@
 import android.provider.DeviceConfig;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -51,10 +52,7 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * BpfNetMaps is responsible for providing traffic controller relevant functionality.
@@ -518,6 +516,14 @@
         }
     }
 
+    private Set<Integer> asSet(final int[] uids) {
+        final Set<Integer> uidSet = new ArraySet<>();
+        for (final int uid: uids) {
+            uidSet.add(uid);
+        }
+        return uidSet;
+    }
+
     /**
      * Replaces the contents of the specified UID-based firewall chain.
      * Enables the chain for specified uids and disables the chain for non-specified uids.
@@ -539,8 +545,8 @@
                 // ConnectivityManager#replaceFirewallChain API
                 throw new IllegalArgumentException("Invalid firewall chain: " + chain);
             }
-            final Set<Integer> uidSet = Arrays.stream(uids).boxed().collect(Collectors.toSet());
-            final Set<Integer> uidSetToRemoveRule = new HashSet<>();
+            final Set<Integer> uidSet = asSet(uids);
+            final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
             try {
                 synchronized (sUidOwnerMap) {
                     sUidOwnerMap.forEach((uid, config) -> {
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index ce8584f..b21c5b4 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -77,6 +77,7 @@
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.Inet6Address
@@ -95,12 +96,11 @@
 import kotlin.test.fail
 
 private const val TAG = "EthernetManagerTest"
-// TODO: try to lower this timeout in the future. Currently, ethernet tests are still flaky because
-// the interface is not ready fast enough (mostly due to the up / up / down / up issue).
-private const val TIMEOUT_MS = 2000L
+private const val TIMEOUT_MS = 1000L
 // Timeout used to confirm no callbacks matching given criteria are received. Must be long enough to
 // process all callbacks including ip provisioning when using the updateConfiguration API.
-private const val NO_CALLBACK_TIMEOUT_MS = 500L
+// Note that increasing this timeout increases the test duration.
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
 
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
         IpConfiguration.ProxySettings.NONE, null, null)
@@ -151,7 +151,9 @@
                 context.getSystemService(TestNetworkManager::class.java)
             }
             tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
-                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+                // setting RS delay to 0 and disabling DAD speeds up tests.
+                tnm.createTapInterface(hasCarrier, false /* bringUp */,
+                        true /* disableIpv6ProvisioningDelay */)
             }
             val mtu = tapInterface.mtu
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
@@ -193,9 +195,35 @@
                 val state: Int,
                 val role: Int,
                 val configuration: IpConfiguration?
-            ) : CallbackEntry()
+            ) : CallbackEntry() {
+                override fun toString(): String {
+                    val stateString = when (state) {
+                        STATE_ABSENT -> "STATE_ABSENT"
+                        STATE_LINK_UP -> "STATE_LINK_UP"
+                        STATE_LINK_DOWN -> "STATE_LINK_DOWN"
+                        else -> state.toString()
+                    }
+                    val roleString = when (role) {
+                        ROLE_NONE -> "ROLE_NONE"
+                        ROLE_CLIENT -> "ROLE_CLIENT"
+                        ROLE_SERVER -> "ROLE_SERVER"
+                        else -> role.toString()
+                    }
+                    return ("InterfaceStateChanged(iface=$iface, state=$stateString, " +
+                            "role=$roleString, ipConfig=$configuration)")
+                }
+            }
 
-            data class EthernetStateChanged(val state: Int) : CallbackEntry()
+            data class EthernetStateChanged(val state: Int) : CallbackEntry() {
+                override fun toString(): String {
+                    val stateString = when (state) {
+                        ETHERNET_STATE_ENABLED -> "ETHERNET_STATE_ENABLED"
+                        ETHERNET_STATE_DISABLED -> "ETHERNET_STATE_DISABLED"
+                        else -> state.toString()
+                    }
+                    return "EthernetStateChanged(state=$stateString)"
+                }
+            }
         }
 
         override fun onInterfaceStateChanged(
@@ -236,11 +264,13 @@
         fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
 
         fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            assertNotNull(eventuallyExpect(createChangeEvent(iface.name, state, role)))
+            val event = createChangeEvent(iface.name, state, role)
+            assertNotNull(eventuallyExpect(event), "Never received expected $event")
         }
 
         fun eventuallyExpect(state: Int) {
-            assertNotNull(eventuallyExpect(EthernetStateChanged(state)))
+            val event = EthernetStateChanged(state)
+            assertNotNull(eventuallyExpect(event), "Never received expected $event")
         }
 
         fun assertNoCallback() {
@@ -306,14 +336,24 @@
         }
     }
 
+    private fun isEthernetSupported() = em != null
+
     @Before
     fun setUp() {
+        assumeTrue(isEthernetSupported())
         setIncludeTestInterfaces(true)
         addInterfaceStateListener(ifaceListener)
+        // Handler.post() events may get processed after native fd events, so it is possible that
+        // RTM_NEWLINK (from a subsequent createInterface() call) arrives before the interface state
+        // listener is registered. This affects the callbacks and breaks the tests.
+        // setEthernetEnabled() will always wait on a callback, so it is used as a barrier to ensure
+        // proper listener registration before proceeding.
+        setEthernetEnabled(true)
     }
 
     @After
     fun tearDown() {
+        if (!isEthernetSupported()) return
         // Reenable ethernet, so ABSENT callbacks are received.
         setEthernetEnabled(true)
 
@@ -496,10 +536,7 @@
         validateListenerOnRegistration(listener1)
 
         // If an interface appears, existing callbacks see it.
-        // TODO: fix the up/up/down/up callbacks and only send down/up.
         val iface2 = createInterface()
-        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
-        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
         listener1.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
         listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
 
@@ -778,7 +815,7 @@
                 .Builder(ETH_REQUEST.networkCapabilities)
                 .addCapability(testCapability)
                 .build()
-        updateConfiguration(iface, STATIC_IP_CONFIGURATION, nc)
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION, nc).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
@@ -796,7 +833,7 @@
         val network = cb.expectAvailable()
         cb.assertNeverLost()
 
-        updateConfiguration(iface, STATIC_IP_CONFIGURATION)
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
@@ -807,6 +844,9 @@
                 STATIC_IP_CONFIGURATION.staticIpConfiguration.ipAddress!!)
     }
 
+    // TODO(b/240323229): This test is currently flaky due to a race between IpClient restarting and
+    // NetworkAgent tearing down the routes. This problem is exacerbated by disabling RS delay.
+    @Ignore
     @Test
     fun testUpdateConfiguration_forOnlyCapabilities() {
         val iface: EthernetTestInterface = createInterface()
@@ -819,7 +859,7 @@
                 .Builder(ETH_REQUEST.networkCapabilities)
                 .addCapability(testCapability)
                 .build()
-        updateConfiguration(iface, capabilities = nc)
+        updateConfiguration(iface, capabilities = nc).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 01db1c3..d7440bb 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3021,6 +3021,43 @@
     }
 
     @Test
+    public void testNetworkDoesntMatchRequestsUntilConnected() throws Exception {
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.requestNetwork(wifiRequest, cb);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        // Updating the score triggers a rematch.
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().build());
+        cb.assertNoCallback();
+        mWiFiNetworkAgent.connect(false);
+        cb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        cb.assertNoCallback();
+        mCm.unregisterNetworkCallback(cb);
+    }
+
+    @Test
+    public void testNetworkNotVisibleUntilConnected() throws Exception {
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, cb);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final NetworkCapabilities nc = mWiFiNetworkAgent.getNetworkCapabilities();
+        nc.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        mWiFiNetworkAgent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
+        cb.assertNoCallback();
+        mWiFiNetworkAgent.connect(false);
+        cb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        final CallbackEntry found = CollectionUtils.findLast(cb.getHistory(),
+                it -> it instanceof CallbackEntry.CapabilitiesChanged);
+        assertTrue(((CallbackEntry.CapabilitiesChanged) found).getCaps()
+                .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        cb.assertNoCallback();
+        mCm.unregisterNetworkCallback(cb);
+    }
+
+    @Test
     public void testStateChangeNetworkCallbacks() throws Exception {
         final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
         final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 1813393..5808beb 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -536,6 +536,25 @@
                 .onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
     }
 
+    @Test
+    public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final INsdManagerCallback cb = getCallback();
+        final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb);
+        deathRecipient.binderDied();
+
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
+        verify(mMockMDnsM, never()).resolve(anyInt() /* id */, anyString() /* serviceName */,
+                anyString() /* registrationType */, anyString() /* domain */,
+                anyInt()/* interfaceIdx */);
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index f159859..1fda04e 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -970,31 +970,6 @@
                 AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
     }
 
-    private void setAppOpsPermission() {
-        doAnswer(invocation -> {
-            when(mAppOps.noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN,
-                    Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */))
-                    .thenReturn(AppOpsManager.MODE_ALLOWED);
-            return null;
-        }).when(mAppOps).setMode(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testProvisionVpnProfileNotPreconsented_withControlVpnPermission() throws Exception {
-        setAppOpsPermission();
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpnAndSetupUidChecks();
-
-        // ACTIVATE_PLATFORM_VPN will be granted if VPN app has CONTROL_VPN permission.
-        checkProvisionVpnProfile(vpn, true /* expectedResult */,
-                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-    }
-
     @Test
     public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 503d920..aad80d5 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -38,9 +37,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
-import android.net.EthernetNetworkManagementException;
 import android.net.EthernetNetworkSpecifier;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -55,7 +52,6 @@
 import android.net.ip.IpClientManager;
 import android.os.Build;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.os.test.TestLooper;
 
@@ -74,9 +70,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
@@ -84,7 +77,6 @@
 public class EthernetNetworkFactoryTest {
     private static final int TIMEOUT_MS = 2_000;
     private static final String TEST_IFACE = "test123";
-    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
     private static final String IP_ADDR = "192.0.2.2/25";
     private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
     private static final String HW_ADDR = "01:02:03:04:05:06";
@@ -241,7 +233,7 @@
         final IpConfiguration ipConfig = createDefaultIpConfig();
         mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
                 createInterfaceCapsBuilder(transportType).build());
-        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true));
 
         ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
                 NetworkOfferCallback.class);
@@ -295,7 +287,7 @@
         // then calling onNetworkUnwanted.
         mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
                 createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
-        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true));
 
         clearInvocations(mIpClient);
         clearInvocations(mNetworkAgent);
@@ -305,81 +297,63 @@
     public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
         initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
         // verify that the IpClient gets shut down when interface state changes to down.
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(ret);
         verify(mIpClient).shutdown();
-        assertEquals(TEST_IFACE, listener.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listenerDown = new TestNetworkManagementListener();
-        final TestNetworkManagementListener listenerUp = new TestNetworkManagementListener();
 
-        final boolean retDown =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listenerDown);
+        final boolean retDown = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(retDown);
         verifyStop();
-        assertEquals(TEST_IFACE, listenerDown.expectOnResult());
 
-        final boolean retUp =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listenerUp);
+        final boolean retUp = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertTrue(retUp);
-        assertEquals(TEST_IFACE, listenerUp.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
         initEthernetNetworkFactory();
         createUnprovisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(ret);
         // There should not be an active IPClient or NetworkAgent.
         verify(mDeps, never()).makeIpClient(any(), any(), any());
         verify(mDeps, never())
                 .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
-        assertEquals(TEST_IFACE, listener.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
         initEthernetNetworkFactory();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
         // if interface was never added, link state cannot be updated.
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
     public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
@@ -571,127 +545,16 @@
         verify(mNetworkAgent).markConnected();
     }
 
-    private static final class TestNetworkManagementListener
-            implements INetworkInterfaceOutcomeReceiver {
-        private final CompletableFuture<String> mResult = new CompletableFuture<>();
-
-        @Override
-        public void onResult(@NonNull String iface) {
-            mResult.complete(iface);
-        }
-
-        @Override
-        public void onError(@NonNull EthernetNetworkManagementException exception) {
-            mResult.completeExceptionally(exception);
-        }
-
-        String expectOnResult() throws Exception {
-            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        }
-
-        void expectOnError() throws Exception {
-            assertThrows(EthernetNetworkManagementException.class, () -> {
-                try {
-                    mResult.get();
-                } catch (ExecutionException e) {
-                    throw e.getCause();
-                }
-            });
-        }
-
-        @Override
-        public IBinder asBinder() {
-            return null;
-        }
-    }
-
-    @Test
-    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
-        initEthernetNetworkFactory();
-        createAndVerifyProvisionedInterface(TEST_IFACE);
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
-
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
-        triggerOnProvisioningSuccess();
-
-        assertEquals(TEST_IFACE, listener.expectOnResult());
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetFactory.removeInterface(TEST_IFACE));
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
-    }
-
-    @Test
-    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
-        initEthernetNetworkFactory();
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener successfulListener =
-                new TestNetworkManagementListener();
-
-        // If two calls come in before the first one completes, the first listener will be aborted
-        // and the second one will be successful.
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> {
-                    mNetFactory.updateInterface(
-                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
-                    triggerOnProvisioningSuccess();
-                });
-
-        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
-        assertEquals(TEST_IFACE, successfulListener.expectOnResult());
-    }
-
-    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
-            @NonNull final String iface,
-            @NonNull final Runnable interruptingRunnable) throws Exception {
-        createAndVerifyProvisionedInterface(iface);
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
-
-        // An active update request will be aborted on interrupt prior to provisioning completion.
-        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
-        interruptingRunnable.run();
-
-        failedListener.expectOnError();
-    }
-
     @Test
     public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities);
         triggerOnProvisioningSuccess();
 
-        assertEquals(TEST_IFACE, listener.expectOnResult());
         verify(mDeps).makeEthernetNetworkAgent(any(), any(),
                 eq(capabilities), any(), any(), any(), any());
         verifyRestart(ipConfiguration);
@@ -703,12 +566,10 @@
         // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities);
 
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
@@ -717,8 +578,8 @@
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
         final IpConfiguration initialIpConfig = createStaticIpConfig();
-        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
-                null /*listener*/);
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/);
+
         triggerOnProvisioningSuccess();
         verifyRestart(initialIpConfig);
 
@@ -729,8 +590,7 @@
 
 
         // verify that sending a null ipConfig does not update the current ipConfig.
-        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
-                null /*listener*/);
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/);
         triggerOnProvisioningSuccess();
         verifyRestart(initialIpConfig);
     }
@@ -739,7 +599,7 @@
     public void testOnNetworkNeededOnStaleNetworkOffer() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, null);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false);
         verify(mNetworkProvider).unregisterNetworkOffer(mNetworkOfferCallback);
         // It is possible that even after a network offer is unregistered, CS still sends it
         // onNetworkNeeded() callbacks.
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index a1d93a0..9bf893a 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -21,6 +21,7 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -209,7 +210,8 @@
                 NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
-                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     private void denyManageEthPermission() {
@@ -285,7 +287,8 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 eq(UPDATE_REQUEST.getIpConfiguration()),
-                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+                eq(UPDATE_REQUEST.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -303,19 +306,20 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 isNull(),
-                eq(ncWithSpecifier), eq(NULL_LISTENER));
+                eq(ncWithSpecifier), any(EthernetCallback.class));
     }
 
     @Test
     public void testEnableInterface() {
         mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     @Test
     public void testDisableInterface() {
         mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), any(EthernetCallback.class));
     }
 
     @Test
@@ -328,7 +332,7 @@
         mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(request.getIpConfiguration()),
-                eq(request.getNetworkCapabilities()), isNull());
+                eq(request.getNetworkCapabilities()), any(EthernetCallback.class));
     }
 
     @Test
@@ -337,7 +341,8 @@
                 NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
-                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -369,7 +374,7 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 eq(request.getIpConfiguration()),
-                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+                eq(request.getNetworkCapabilities()), any(EthernetCallback.class));
     }
 
     @Test
@@ -379,7 +384,8 @@
         denyManageEthPermission();
 
         mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -389,7 +395,8 @@
         denyManageEthPermission();
 
         mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     private void denyPermissions(String... permissions) {
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index 0376a2a..082a016 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -39,7 +39,6 @@
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
@@ -76,7 +75,7 @@
     private static final String TEST_IFACE = "test123";
     private static final int TIMEOUT_MS = 1_000;
     private static final String THREAD_NAME = "EthernetServiceThread";
-    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final EthernetCallback NULL_CB = new EthernetCallback(null);
     private EthernetTracker tracker;
     private HandlerThread mHandlerThread;
     @Mock private Context mContext;
@@ -88,7 +87,7 @@
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         initMockResources();
-        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
+        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean())).thenReturn(false);
         when(mNetd.interfaceGetList()).thenReturn(new String[0]);
         mHandlerThread = new HandlerThread(THREAD_NAME);
         mHandlerThread.start();
@@ -344,31 +343,29 @@
                 new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
         final IpConfiguration ipConfig =
                 new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
-        final INetworkInterfaceOutcomeReceiver listener = null;
+        final EthernetCallback listener = new EthernetCallback(null);
 
         tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
         waitForIdle();
 
         verify(mFactory).updateInterface(
-                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities));
     }
 
     @Test
     public void testEnableInterfaceCorrectlyCallsFactory() {
-        tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
+        tracker.enableInterface(TEST_IFACE, NULL_CB);
         waitForIdle();
 
-        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
-                eq(NULL_LISTENER));
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */));
     }
 
     @Test
     public void testDisableInterfaceCorrectlyCallsFactory() {
-        tracker.disableInterface(TEST_IFACE, NULL_LISTENER);
+        tracker.disableInterface(TEST_IFACE, NULL_CB);
         waitForIdle();
 
-        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
-                eq(NULL_LISTENER));
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */));
     }
 
     @Test