Add unit test of tethering local network agent feature

Test: atest TetheringTests:android.net.ip.IpServerTest \
      ConnectivityCoverageTests:android.net.ip.IpServerTest \
      ConnectivityCoverageTests:com.android.networkstack.tethering \
      --update-device
      (Also on the next release config)
Bug: 349487600
Change-Id: I56e0ec106f9398225b162fa94e3ccdb94f413049
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 84b301f..c935cbf35 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -28,6 +28,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.ip.IpServer.STATE_AVAILABLE;
@@ -37,7 +38,9 @@
 import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -53,6 +56,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -64,7 +68,7 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-import android.app.usage.NetworkStatsManager;
+import android.content.Context;
 import android.net.INetd;
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
@@ -72,6 +76,8 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkAgent;
 import android.net.RouteInfo;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -83,8 +89,10 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.test.TestLooper;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -100,6 +108,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -122,6 +131,16 @@
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
+    final ArrayMap<String, Boolean> mFeatureFlags = new ArrayMap<>();
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @Rule
+    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+            new SetFeatureFlagsRule((name, enabled) -> {
+                mFeatureFlags.put(name, enabled);
+                return null;
+            }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
     private static final String IFACE_NAME = "testnet1";
     private static final String UPSTREAM_IFACE = "upstream0";
     private static final String UPSTREAM_IFACE2 = "upstream1";
@@ -164,6 +183,7 @@
             new LinkAddress("2001:db8:0:abcd::168/64"));
     private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
             new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+    private static final int TEST_NET_ID = 123;
 
     @Mock private INetd mNetd;
     @Mock private IpServer.Callback mCallback;
@@ -173,10 +193,11 @@
     @Mock private RouterAdvertisementDaemon mRaDaemon;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
-    @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private BpfCoordinator mBpfCoordinator;
+    @Mock private Context mContext;
+    @Mock private NetworkAgent mNetworkAgent;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
@@ -205,6 +226,18 @@
         when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
         when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
         when(mDependencies.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
+        doAnswer(
+                invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), false)
+        ).when(mDependencies).isFeatureEnabled(any(), anyString());
+        if (isAtLeastV()) {
+            when(mDependencies.makeNetworkAgent(any(), any(), anyString(), anyInt(), any()))
+                    .thenReturn(mNetworkAgent);
+            // Mock the returned network and modifying the status.
+            final Network network = mock(Network.class);
+            doReturn(TEST_NET_ID).when(network).getNetId();
+            doReturn(network).when(mNetworkAgent).register();
+            doReturn(network).when(mNetworkAgent).getNetwork();
+        }
 
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
         mInterfaceConfiguration.flags = new String[0];
@@ -294,10 +327,9 @@
     private IpServer createIpServer(final int interfaceType) {
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
-        return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mRoutingCoordinatorManager, mCallback, mTetherConfig,
+        return new IpServer(IFACE_NAME, mContext, mHandler, interfaceType, mSharedLog, mNetd,
+                mBpfCoordinator, mRoutingCoordinatorManager, mCallback, mTetherConfig,
                 mTetheringMetrics, mDependencies);
-
     }
 
     @Test
@@ -342,6 +374,10 @@
         verifyNoMoreInteractions(mNetd, mCallback);
     }
 
+    private boolean isTetheringNetworkAgentFeatureEnabled() {
+        return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false);
+    }
+
     @Test
     public void canBeTetheredAsBluetooth() throws Exception {
         initStateMachine(TETHERING_BLUETOOTH);
@@ -360,10 +396,16 @@
                     IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         }
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
-        inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
-        // One for ipv4 route, one for ipv6 link local route.
-        inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
-                any(), any());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never())
+                    .networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+            // One for ipv4 route, one for ipv6 link local route.
+            inOrder.verify(mNetd, times(2))
+                    .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any());
+        }
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
@@ -379,7 +421,11 @@
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
-        inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        }
         // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
         // happen after T. Before T, the interface configuration control in bluetooth side.
         if (isAtLeastT()) {
@@ -411,9 +457,15 @@
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
                 IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
-        inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
-        inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
-                any(), any());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never())
+                    .networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+            inOrder.verify(mNetd, times(2))
+                    .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any());
+        }
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
@@ -592,7 +644,11 @@
         inOrder.verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
-        inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        }
         inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mRoutingCoordinatorManager).releaseDownstream(any());
@@ -1058,6 +1114,162 @@
         return true;
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_tetheringAgentEnabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, true);
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT, enabled = false)
+    @Test
+    public void testTetheringNetworkAgent_tetheringAgentDisabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false);
+    }
+
+    // Verify Tethering Network Agent feature doesn't affect Wi-fi P2P Group Owner although
+    // the code is mostly shared.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_p2pGroupOwnerAgentDisabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false);
+    }
+
+    private void doTestTetheringNetworkAgent(int scope, boolean expectAgentEnabled)
+            throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(scope));
+
+        inOrder.verify(mNetworkAgent, expectAgentEnabled ? times(1) : never()).register();
+        inOrder.verify(mNetd, times(1)).tetherInterfaceAdd(anyString());
+        if (expectAgentEnabled) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+            inOrder.verify(mNetworkAgent, times(1)).sendLinkProperties(any());
+            inOrder.verify(mNetworkAgent, times(1)).markConnected();
+        } else {
+            inOrder.verify(mNetd, times(1)).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, times(2)).networkAddRoute(anyInt(), anyString(), any(), any());
+            inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+            inOrder.verify(mNetworkAgent, never()).markConnected();
+        }
+
+        dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+        if (expectAgentEnabled) {
+            inOrder.verify(mNetworkAgent, times(1)).unregister();
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetworkAgent, never()).unregister();
+            inOrder.verify(mNetd, times(1)).networkRemoveInterface(anyInt(), anyString());
+        }
+    }
+
+    // Verify if the registration failed, tethering can be gracefully shutdown.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_registerThrows() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd, mCallback);
+        doReturn(null).when(mNetworkAgent).getNetwork();
+        doThrow(IllegalStateException.class).when(mNetworkAgent).register();
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+        inOrder.verify(mNetworkAgent, never()).markConnected();
+        inOrder.verify(mNetworkAgent, never()).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+        inOrder.verify(mCallback).updateInterfaceState(
+                mIpServer, STATE_AVAILABLE, TETHER_ERROR_SERVICE_UNAVAIL);
+    }
+
+    // Verify if the network creation failed, tethering can be gracefully shutdown.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_netdThrows() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd, mCallback);
+        doThrow(ServiceSpecificException.class).when(mNetd).tetherInterfaceAdd(any());
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+        inOrder.verify(mNetworkAgent, never()).markConnected();
+        inOrder.verify(mNetworkAgent).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+        inOrder.verify(mCallback).updateInterfaceState(
+                mIpServer, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+    }
+
+    // Verify when IPv6 address update, set routes accordingly.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_ipv6AddressUpdate() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
+        // Ipv6 link local route won't show up in the LinkProperties, so just
+        // verify ipv4 route.
+        final ArgumentCaptor<LinkProperties> lpCaptor =
+                ArgumentCaptor.forClass(LinkProperties.class);
+        inOrder.verify(mNetworkAgent).sendLinkProperties(lpCaptor.capture());
+        final RouteInfo expectedIpv4Route = new RouteInfo(PrefixUtils.asIpPrefix(mTestAddress),
+                null, IFACE_NAME, RouteInfo.RTN_UNICAST);
+        assertRoutes(List.of(expectedIpv4Route), lpCaptor.getValue().getRoutes());
+        assertEquals(IFACE_NAME, lpCaptor.getValue().getInterfaceName());
+
+        inOrder.verify(mNetworkAgent).markConnected();
+
+        // Mock ipv4-only upstream show up.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+        inOrder.verifyNoMoreInteractions();
+
+        // Verify LinkProperties is updated when IPv6 connectivity is available.
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(UPSTREAM_IFACE);
+        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+        inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+        inOrder.verify(mNetworkAgent).sendLinkProperties(lpCaptor.capture());
+
+        // Expect one Ipv4 route, plus one Ipv6 route.
+        final RouteInfo expectedIpv6Route = new RouteInfo(UPSTREAM_PREFIXES.toArray(
+                new IpPrefix[0])[0], null, IFACE_NAME, RouteInfo.RTN_UNICAST);
+        assertRoutes(List.of(expectedIpv4Route, expectedIpv6Route),
+                lpCaptor.getValue().getRoutes());
+        assertEquals(IFACE_NAME, lpCaptor.getValue().getInterfaceName());
+
+        dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+        inOrder.verify(mNetworkAgent).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+    }
+
+    private void assertRoutes(List<RouteInfo> expectedRoutes, List<RouteInfo> actualRoutes) {
+        assertTrue("Expected Routes: " + expectedRoutes + ", but got: " + actualRoutes,
+                expectedRoutes.equals(actualRoutes));
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
     public void dadProxyUpdates() throws Exception {
         InOrder inOrder = inOrder(mDadProxy);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index dc3cbd2..d659815 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -75,6 +75,7 @@
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
@@ -83,6 +84,7 @@
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
 import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -192,6 +194,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
@@ -219,6 +222,7 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.MiscAsserts;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -250,6 +254,16 @@
 public class TetheringTest {
     @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
+    final ArrayMap<String, Boolean> mFeatureFlags = new ArrayMap<>();
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @Rule
+    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+            new SetFeatureFlagsRule((name, enabled) -> {
+                mFeatureFlags.put(name, enabled);
+                return null;
+            }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
     private static final int IFINDEX_OFFSET = 100;
 
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -461,6 +475,11 @@
         public void setOnDhcpServerCreatedResult(final int result) {
             mOnDhcpServerCreatedResult = result;
         }
+
+        @Override
+        public boolean isFeatureEnabled(Context context, String name) {
+            return mFeatureFlags.getOrDefault(name, false);
+        }
     }
 
     public class MockTetheringDependencies extends TetheringDependencies {
@@ -954,12 +973,18 @@
         verifyNoMoreInteractions(mCm);
     }
 
-    private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
+    private void verifyInterfaceServingModeStarted(String ifname, boolean expectAgentEnabled)
+            throws Exception {
         verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
         verify(mNetd).tetherInterfaceAdd(ifname);
-        verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, ifname);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname),
-                anyString(), anyString());
+        if (expectAgentEnabled) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, ifname);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname),
+                    anyString(), anyString());
+        }
     }
 
     private void verifyTetheringBroadcast(String ifname, String whichExtra) {
@@ -1061,10 +1086,18 @@
         failingLocalOnlyHotspotLegacyApBroadcast(false);
     }
 
-    private void verifyStopHotpot() throws Exception {
+    private boolean isTetheringNetworkAgentFeatureEnabled() {
+        return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false);
+    }
+
+    private void verifyStopHotpot(boolean isLocalOnly) throws Exception {
         verify(mNetd).tetherApplyDnsInterfaces();
         verify(mNetd).tetherInterfaceRemove(TEST_WLAN_IFNAME);
-        verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        if (!isLocalOnly && isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        }
         // interfaceSetCfg() called once for enabling and twice disabling IPv4.
         verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
         verify(mNetd).tetherStop();
@@ -1083,7 +1116,8 @@
     }
 
     private void verifyStartHotspot(boolean isLocalOnly) throws Exception {
-        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+        final boolean expectAgentEnabled = !isLocalOnly && isTetheringNetworkAgentFeatureEnabled();
+        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME, expectAgentEnabled);
         verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
@@ -1127,7 +1161,7 @@
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
-        verifyStopHotpot();
+        verifyStopHotpot(true /* isLocalOnly */);
     }
 
     /**
@@ -2073,7 +2107,7 @@
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
-        verifyStopHotpot();
+        verifyStopHotpot(false /* isLocalOnly */);
     }
 
     @Test
@@ -2218,9 +2252,14 @@
         // code is refactored the two calls during shutdown will revert to one.
         verify(mNetd, times(3)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName)));
         verify(mNetd, times(1)).tetherInterfaceAdd(TEST_WLAN_IFNAME);
-        verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
+                    anyString(), anyString());
+        }
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
         verify(mWifiManager).updateInterfaceIpState(
@@ -2239,7 +2278,11 @@
         // so it can take down AP mode.
         verify(mNetd, times(1)).tetherApplyDnsInterfaces();
         verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME);
-        verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        }
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
 
@@ -3100,7 +3143,7 @@
         }
         sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
 
-        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME, false);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
         verify(mNetd, times(1)).tetherStartWithConfiguration(any());
@@ -4119,13 +4162,22 @@
                     && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP)));
         }
         verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
-        verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                    anyString(), anyString());
+        }
         verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
         verify(mNetd).tetherStartWithConfiguration(any());
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                    anyString(), anyString());
+        }
         verifyNoMoreInteractions(mNetd);
         reset(mNetd);
     }
@@ -4140,7 +4192,11 @@
     private void verifyNetdCommandForBtTearDown() throws Exception {
         verify(mNetd).tetherApplyDnsInterfaces();
         verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
-        verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        }
         // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
         // happen after T. Before T, the interface configuration control in bluetooth side.
         verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
@@ -4405,7 +4461,7 @@
         initTetheringOnTestThread();
         // Enable wifi P2P.
         sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
-        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME, false);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
         verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();