Updates so Network Mgmt Callbacks Are Sent am: 724a0aea08

Original change: https://android-review.googlesource.com/c/platform/frameworks/opt/net/ethernet/+/1949595

Change-Id: I02c713ea6b1d173982b7abe45b535fc459fa92bc
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();