Updates so Network Mgmt Callbacks Are Sent

Updates so that network management API updates in the ethernet stack can
handle concurrent requests correctly in regards to notifying caller
supplied callbacks.

Bug: 210485380
Test: atest EthernetServiceTests
Change-Id: I8073251b2c63309da031c2b1c98d61614deadfe8
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 7b727a3..c610f00 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -22,10 +22,12 @@
 import android.net.ConnectivityManager;
 import android.net.EthernetNetworkSpecifier;
 import android.net.IInternalNetworkManagementListener;
+import android.net.InternalNetworkManagementException;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
 import android.net.LinkProperties;
+import android.net.Network;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkFactory;
@@ -203,14 +205,9 @@
             @Nullable final IInternalNetworkManagementListener listener) {
         enforceInterfaceIsTracked(ifaceName);
         final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
-        // TODO: The listener will have issues if called in quick succession for the same interface
-        //  before the IP layer restarts. Update the listener logic to address multiple successive
-        //  calls for a particular interface.
-        iface.mNetworkManagementListener = listener;
-        if (iface.updateInterface(ipConfig, capabilities)) {
-            mTrackingInterfaces.put(ifaceName, iface);
-            updateCapabilityFilter();
-        }
+        iface.updateInterface(ipConfig, capabilities, listener);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
     }
 
     private void enforceInterfaceIsTracked(@NonNull final String ifaceName) {
@@ -247,6 +244,7 @@
     void removeInterface(String interfaceName) {
         NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
         if (iface != null) {
+            iface.maybeSendNetworkManagementCallbackForAbort();
             iface.stop();
         }
 
@@ -302,6 +300,21 @@
         return network;
     }
 
+    private static void maybeSendNetworkManagementCallback(
+            @Nullable final IInternalNetworkManagementListener listener,
+            @Nullable final Network network,
+            @Nullable final InternalNetworkManagementException e) {
+        if (null == listener) {
+            return;
+        }
+
+        try {
+            listener.onComplete(network, e);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Can't send onComplete for network management callback", re);
+        }
+    }
+
     @VisibleForTesting
     static class NetworkInterfaceState {
         final String name;
@@ -320,8 +333,7 @@
 
         private volatile @Nullable IpClientManager mIpClient;
         private @NonNull NetworkCapabilities mCapabilities;
-        private @Nullable IpClientCallbacksImpl mIpClientCallback;
-        private @Nullable IInternalNetworkManagementListener mNetworkManagementListener;
+        private @Nullable EthernetIpClientCallback mIpClientCallback;
         private @Nullable EthernetNetworkAgent mNetworkAgent;
         private @Nullable IpConfiguration mIpConfig;
 
@@ -348,9 +360,14 @@
 
         long refCount = 0;
 
-        private class IpClientCallbacksImpl extends IpClientCallbacks {
+        private class EthernetIpClientCallback extends IpClientCallbacks {
             private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
             private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
+            @Nullable IInternalNetworkManagementListener mNetworkManagementListener;
+
+            EthernetIpClientCallback(@Nullable final IInternalNetworkManagementListener listener) {
+                mNetworkManagementListener = listener;
+            }
 
             @Override
             public void onIpClientCreated(IIpClient ipClient) {
@@ -368,12 +385,12 @@
 
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
-                mHandler.post(() -> onIpLayerStarted(newLp));
+                mHandler.post(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
             }
 
             @Override
             public void onProvisioningFailure(LinkProperties newLp) {
-                mHandler.post(() -> onIpLayerStopped(newLp));
+                mHandler.post(() -> onIpLayerStopped(mNetworkManagementListener));
             }
 
             @Override
@@ -431,30 +448,25 @@
             mLegacyType = getLegacyType(mCapabilities);
         }
 
-        boolean updateInterface(@NonNull final IpConfiguration ipConfig,
-                @Nullable final NetworkCapabilities capabilities) {
-            final boolean shouldUpdateIpConfig = !Objects.equals(mIpConfig, ipConfig);
-            final boolean shouldUpdateCapabilities = null != capabilities
-                    && !Objects.equals(mCapabilities, capabilities);
+        void updateInterface(@NonNull final IpConfiguration ipConfig,
+                @Nullable final NetworkCapabilities capabilities,
+                @Nullable final IInternalNetworkManagementListener listener) {
             if (DBG) {
                 Log.d(TAG, "updateInterface, iface: " + name
-                        + ", shouldUpdateIpConfig: " + shouldUpdateIpConfig
-                        + ", shouldUpdateCapabilities: " + shouldUpdateCapabilities
                         + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
                         + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
+                        + ", listener: " + listener
                 );
             }
 
-            if (shouldUpdateIpConfig) { mIpConfig = ipConfig; };
-            if (shouldUpdateCapabilities) { setCapabilities(capabilities); };
-            if (shouldUpdateIpConfig || shouldUpdateCapabilities) {
-                // 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();
-                return true;
-            }
-            return false;
+            mIpConfig = ipConfig;
+            setCapabilities(capabilities);
+            // Send an abort callback if a request is filed before the previous one has completed.
+            maybeSendNetworkManagementCallbackForAbort();
+            // 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);
         }
 
         boolean isRestricted() {
@@ -462,6 +474,10 @@
         }
 
         private void start() {
+            start(null);
+        }
+
+        private void start(@Nullable final IInternalNetworkManagementListener listener) {
             if (mIpClient != null) {
                 if (DBG) Log.d(TAG, "IpClient already started");
                 return;
@@ -470,9 +486,10 @@
                 Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
             }
 
-            mIpClientCallback = new IpClientCallbacksImpl();
+            mIpClientCallback = new EthernetIpClientCallback(listener);
             mDeps.makeIpClient(mContext, name, mIpClientCallback);
             mIpClientCallback.awaitIpClientStart();
+
             if (sTcpBufferSizes == null) {
                 sTcpBufferSizes = mContext.getResources().getString(
                         com.android.internal.R.string.config_ethernet_tcp_buffers);
@@ -480,8 +497,13 @@
             provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
         }
 
-        void onIpLayerStarted(LinkProperties linkProperties) {
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
+                @Nullable final IInternalNetworkManagementListener listener) {
             if(mIpClient == null) {
+                // This call comes from a message posted on the handler thread, but the IpClient has
+                // since been stopped such as may be the case if updateInterfaceLinkState() is
+                // queued on the handler thread prior. As network management callbacks are sent in
+                // stop(), there is no need to send them again here.
                 if (DBG) Log.d(TAG, "IpClient is not initialized.");
                 return;
             }
@@ -516,33 +538,53 @@
                     });
             mNetworkAgent.register();
             mNetworkAgent.markConnected();
-            sendNetworkManagementCallback();
+            realizeNetworkManagementCallback(mNetworkAgent.getNetwork(), null);
         }
 
-        private void sendNetworkManagementCallback() {
-            if (null != mNetworkManagementListener) {
-                try {
-                    mNetworkManagementListener.onComplete(mNetworkAgent.getNetwork(), null);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Can't send onComplete for network management callback", e);
-                } finally {
-                    mNetworkManagementListener = null;
-                }
-            }
-        }
-
-        void onIpLayerStopped(LinkProperties linkProperties) {
+        void onIpLayerStopped(@Nullable final IInternalNetworkManagementListener listener) {
+            // This cannot happen due to provisioning timeout, because our timeout is 0. It can
+            // happen due to errors while provisioning or on provisioning loss.
             if(mIpClient == null) {
                 if (DBG) Log.d(TAG, "IpClient is not initialized.");
                 return;
             }
-            // This cannot happen due to provisioning timeout, because our timeout is 0. It can only
-            // happen if we're provisioned and we lose provisioning.
-            stop();
-            // If the interface has disappeared provisioning will fail over and over again, so
-            // there is no point in starting again
-            if (null != mDeps.getNetworkInterfaceByName(name)) {
-                start();
+            // 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 InternalNetworkManagementException(
+                            "The IP provisioning request has been aborted."));
+        }
+
+        // Must be called on the handler thread
+        private void realizeNetworkManagementCallback(@Nullable final Network network,
+                @Nullable final InternalNetworkManagementException e) {
+            ensureRunningOnEthernetHandlerThread();
+            if (null == mIpClientCallback) {
+                return;
+            }
+
+            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
+                    mIpClientCallback.mNetworkManagementListener, network, e);
+            // Only send a single callback per listener.
+            mIpClientCallback.mNetworkManagementListener = null;
+        }
+
+        private void ensureRunningOnEthernetHandlerThread() {
+            if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+                throw new IllegalStateException(
+                        "Not running on the Ethernet thread: "
+                                + Thread.currentThread().getName());
             }
         }
 
@@ -577,8 +619,11 @@
             if (mLinkUp == up) return false;
             mLinkUp = up;
 
-            stop();
-            if (up) {
+            if (!up) { // was up, goes down
+                maybeSendNetworkManagementCallbackForAbort();
+                stop();
+            } else { // was down, goes up
+                stop();
                 start();
             }
 
@@ -627,10 +672,14 @@
                         .build();
         }
 
-        void restart(){
+        void restart() {
+            restart(null);
+        }
+
+        void restart(@Nullable final IInternalNetworkManagementListener listener){
             if (DBG) Log.d(TAG, "reconnecting Ethernet");
             stop();
-            start();
+            start(listener);
         }
 
         @Override
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 52ddf3c..d569990 100644
--- a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -16,8 +16,11 @@
 
 package com.android.server.ethernet;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -63,6 +66,8 @@
 import com.android.internal.R;
 import com.android.net.module.util.InterfaceParams;
 
+import com.android.testutils.DevSdkIgnoreRule;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -72,9 +77,7 @@
 
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -84,7 +87,7 @@
     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";
-    private final TestLooper mLooper = new TestLooper();
+    private TestLooper mLooper;
     private Handler mHandler;
     private EthernetNetworkFactory mNetFactory = null;
     private IpClientCallbacks mIpClientCallbacks;
@@ -99,14 +102,18 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mHandler = new Handler(mLooper.getLooper());
-        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
-
         setupNetworkAgentMock();
         setupIpClientMock();
         setupContext();
     }
 
+    //TODO: Move away from usage of TestLooper in order to move this logic back into @Before.
+    private void initEthernetNetworkFactory() {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+    }
+
     private void setupNetworkAgentMock() {
         when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()))
                 .thenAnswer(new AnswerWithArguments() {
@@ -288,6 +295,7 @@
 
     @Test
     public void testAcceptRequest() throws Exception {
+        initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
         assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
 
@@ -299,6 +307,7 @@
 
     @Test
     public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
+        initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
         // verify that the IpClient gets shut down when interface state changes to down.
         assertTrue(mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
@@ -307,6 +316,7 @@
 
     @Test
     public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         assertTrue(mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
         verifyStop();
@@ -314,6 +324,7 @@
 
     @Test
     public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
         createUnprovisionedInterface(TEST_IFACE);
         assertTrue(mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
         // There should not be an active IPClient or NetworkAgent.
@@ -324,6 +335,7 @@
 
     @Test
     public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
         // if interface was never added, link state cannot be updated.
         assertFalse(mNetFactory.updateInterfaceLinkState("eth1", true));
         verify(mDeps, never()).makeIpClient(any(), any(), any());
@@ -331,6 +343,7 @@
 
     @Test
     public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         mNetFactory.needNetworkFor(createDefaultRequest());
         verify(mIpClient, never()).startProvisioning(any());
@@ -338,6 +351,7 @@
 
     @Test
     public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
         createUnprovisionedInterface(TEST_IFACE);
         mNetFactory.needNetworkFor(createDefaultRequest());
         verify(mIpClient).startProvisioning(any());
@@ -348,6 +362,7 @@
 
     @Test
     public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
         mNetFactory.needNetworkFor(createDefaultRequest());
         verify(mIpClient, never()).startProvisioning(any());
@@ -358,6 +373,7 @@
 
     @Test
     public void testProvisioningLoss() throws Exception {
+        initEthernetNetworkFactory();
         when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
@@ -369,18 +385,24 @@
 
     @Test
     public void testProvisioningLossForDisappearedInterface() throws Exception {
+        initEthernetNetworkFactory();
         // mocked method returns null by default, but just to be explicit in the test:
         when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null);
 
         createAndVerifyProvisionedInterface(TEST_IFACE);
         triggerOnProvisioningFailure();
-        verifyStop();
+
         // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
         verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+        verify(mIpClient, never()).startProvisioning(any());
     }
 
     @Test
     public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        initEthernetNetworkFactory();
         createUnprovisionedInterface(TEST_IFACE);
         mNetFactory.updateInterfaceLinkState(TEST_IFACE, false);
 
@@ -399,6 +421,7 @@
 
     @Test
     public void testLinkPropertiesChanged() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
         LinkProperties lp = new LinkProperties();
@@ -409,6 +432,7 @@
 
     @Test
     public void testNetworkUnwanted() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
         mNetworkAgent.getCallbacks().onNetworkUnwanted();
@@ -418,6 +442,7 @@
 
     @Test
     public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        initEthernetNetworkFactory();
         // ensures provisioning is restarted after provisioning loss
         when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
         createAndVerifyProvisionedInterface(TEST_IFACE);
@@ -441,6 +466,7 @@
 
     @Test
     public void testTransportOverrideIsCorrectlySet() throws Exception {
+        initEthernetNetworkFactory();
         // createProvisionedInterface() has verifications in place for transport override
         // functionality which for EthernetNetworkFactory is network score and legacy type mappings.
         createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET,
@@ -461,6 +487,7 @@
 
     @Test
     public void testReachabilityLoss() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
         triggerOnReachabilityLost();
@@ -471,6 +498,7 @@
 
     @Test
     public void testIgnoreOnIpLayerStartedCallbackAfterIpClientHasStopped() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
         mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
@@ -484,6 +512,7 @@
 
     @Test
     public void testIgnoreOnIpLayerStoppedCallbackAfterIpClientHasStopped() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
         mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
@@ -497,26 +526,37 @@
 
     @Test
     public void testIgnoreLinkPropertiesCallbackAfterIpClientHasStopped() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         LinkProperties lp = new LinkProperties();
 
-        mIpClientCallbacks.onProvisioningFailure(lp);
+        // The test requires the two proceeding methods to happen one after the other in ENF and
+        // verifies onLinkPropertiesChange doesn't complete execution for a downed interface.
+        // Posting is necessary as updateInterfaceLinkState with false will set mIpClientCallbacks
+        // to null which will throw an NPE in the test if executed synchronously.
+        mHandler.post(() -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
         mIpClientCallbacks.onLinkPropertiesChange(lp);
         mLooper.dispatchAll();
-        verifyStop();
 
+        verifyStop();
         // ipClient has been shut down first, we should not update
         verify(mNetworkAgent, never()).sendLinkPropertiesImpl(same(lp));
     }
 
     @Test
     public void testIgnoreNeighborLossCallbackAfterIpClientHasStopped() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+
+        // The test requires the two proceeding methods to happen one after the other in ENF and
+        // verifies onReachabilityLost doesn't complete execution for a downed interface.
+        // Posting is necessary as updateInterfaceLinkState with false will set mIpClientCallbacks
+        // to null which will throw an NPE in the test if executed synchronously.
+        mHandler.post(() -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
         mIpClientCallbacks.onReachabilityLost("Neighbor Lost");
         mLooper.dispatchAll();
-        verifyStop();
 
+        verifyStop();
         // ipClient has been shut down first, we should not update
         verify(mIpClient, never()).startProvisioning(any());
         verify(mNetworkAgent, never()).register();
@@ -567,6 +607,7 @@
 
     @Test
     public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();
@@ -580,8 +621,71 @@
         assertNull(ret.second);
     }
 
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @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();
+                });
+
+        final Pair<Network, InternalNetworkManagementException> successfulResult =
+                successfulListener.expectOnComplete();
+        assertEquals(mMockNetwork, successfulResult.first);
+        assertNull(successfulResult.second);
+    }
+
+    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();
+
+        final Pair<Network, InternalNetworkManagementException> failedResult =
+                failedListener.expectOnComplete();
+        assertNull(failedResult.first);
+        assertNotNull(failedResult.second);
+        assertTrue(failedResult.second.getMessage().contains("aborted"));
+    }
+
     @Test
     public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
+        initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();