diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index a651b1b198..baaaef5092 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -43,6 +43,7 @@ import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT; import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM; +import static com.android.networkstack.tethering.TetheringConfiguration.WIFIP2PGO_LOCAL_NETWORK_AGENT; import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix; import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER; import static com.android.networkstack.tethering.util.TetheringUtils.getTransportTypeForTetherableType; @@ -334,7 +335,8 @@ public class IpServer extends StateMachineShim { private final Handler mHandler; private final Context mContext; - private final boolean mSupportLocalAgent; + private final boolean mSupportTetheringLocalAgent; + private final boolean mSupportWifiP2pGroupOwnerLocalAgent; // This will be null if the TetheredState is not entered or feature not supported. // This will be only accessed from the IpServer handler thread. @@ -380,8 +382,10 @@ public class IpServer extends StateMachineShim { mServingMode = STATE_AVAILABLE; // Tethering network agent is supported on V+, and will be rolled out gradually. - mSupportLocalAgent = SdkLevel.isAtLeastV() + mSupportTetheringLocalAgent = SdkLevel.isAtLeastV() && mDeps.isFeatureEnabled(mContext, TETHERING_LOCAL_NETWORK_AGENT); + mSupportWifiP2pGroupOwnerLocalAgent = mSupportTetheringLocalAgent + && mDeps.isFeatureEnabled(mContext, WIFIP2PGO_LOCAL_NETWORK_AGENT); mInitialState = new InitialState(); mLocalHotspotState = new LocalHotspotState(); @@ -1162,9 +1166,8 @@ public class IpServer extends StateMachineShim { @SuppressLint("NewApi") private void startServingInterface() { - // TODO: Enable Network Agent for Wifi P2P Group Owner mode when Network Agent - // for Group Client mode is supported. - if (mSupportLocalAgent && getScope() == CONNECTIVITY_SCOPE_GLOBAL) { + if (mSupportTetheringLocalAgent && (mSupportWifiP2pGroupOwnerLocalAgent + || getScope() != CONNECTIVITY_SCOPE_LOCAL)) { try { mTetheringAgent = mDeps.makeNetworkAgent(mContext, Looper.myLooper(), TAG, mInterfaceType, mLinkProperties); diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java index 7c61b9c442..4f9916c16b 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java @@ -157,6 +157,13 @@ public class TetheringConfiguration { */ public static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent"; + /** + * A feature flag to control whether the Wifi P2P Group Owner local network agent + * should be enabled. Disabled by default. This depends on TETHERING_LOCAL_AGENT + * and will not be enabled if that flag is off. + */ + public static final String WIFIP2PGO_LOCAL_NETWORK_AGENT = "wifip2pgo_local_network_agent"; + public final String[] tetherableUsbRegexs; public final String[] tetherableWifiRegexs; public final String[] tetherableWigigRegexs; diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java index d47f4b3294..a2bf3a6c99 100644 --- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java +++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java @@ -219,6 +219,7 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { // Shamelessly copied from TetheringConfiguration. private static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent"; + private static final String WIFIP2PGO_LOCAL_NETWORK_AGENT = "wifip2pgo_local_network_agent"; @After public void tearDown() throws Exception { @@ -1245,15 +1246,29 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { } } + private void assumeMatchNonThreadLocalNetworksEnabled() { + assumeTrue(runAsShell(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE, + () -> CompatChanges.isChangeEnabled(ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS))); + } + @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Test - public void testLocalAgent_networkCallbacks() throws Exception { - final boolean isMatchNonThreadLocalNetworksEnabled = runAsShell( - READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE, - () -> CompatChanges.isChangeEnabled(ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)); - assumeTrue(isMatchNonThreadLocalNetworksEnabled); - + public void testTetheringLocalAgent_networkCallbacks() throws Exception { + assumeMatchNonThreadLocalNetworksEnabled(); mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, TETHERING_LOCAL_NETWORK_AGENT, "1"); + doTestLocalAgent_networkCallbacks(CONNECTIVITY_SCOPE_GLOBAL); + } + + @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Test + public void testWifiP2pGroupOwnerLocalAgent_networkCallbacks() throws Exception { + assumeMatchNonThreadLocalNetworksEnabled(); + mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, TETHERING_LOCAL_NETWORK_AGENT, "1"); + mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, WIFIP2PGO_LOCAL_NETWORK_AGENT, "1"); + doTestLocalAgent_networkCallbacks(CONNECTIVITY_SCOPE_LOCAL); + } + + private void doTestLocalAgent_networkCallbacks(int scope) throws Exception { assumeFalse(isInterfaceForTetheringAvailable()); setIncludeTestInterfaces(true); @@ -1273,9 +1288,13 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { downstreamIface.getInterfaceName(), iface); final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET) - .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL).build(); + .setConnectivityScope(scope).build(); tetheringEventCallback = enableTethering(iface, request, null /* any upstream */); - tetheringEventCallback.awaitInterfaceTethered(); + if (scope == CONNECTIVITY_SCOPE_GLOBAL) { + tetheringEventCallback.awaitInterfaceTethered(); + } else { + tetheringEventCallback.awaitInterfaceLocalOnly(); + } // Verify NetworkCallback works accordingly. final Network network = networkCallback.expect(CallbackEntry.AVAILABLE).getNetwork(); @@ -1284,6 +1303,15 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { assertEquals(network, capEvent.getNetwork()); assertTrue(capEvent.getCaps().hasTransport(TRANSPORT_ETHERNET)); assertTrue(capEvent.getCaps().hasCapability(NET_CAPABILITY_LOCAL_NETWORK)); + + // Verify details in the LinkPropertiesChanged event. + final CallbackEntry.LinkPropertiesChanged lpEvent = + networkCallback.eventuallyExpect(CallbackEntry.LINK_PROPERTIES_CHANGED); + assertEquals(network, lpEvent.getNetwork()); + assertEquals(iface, lpEvent.getLp().getInterfaceName()); + assertTrue(lpEvent.getLp().hasIPv4Address()); + // At least one Ipv4 route for the subnet is needed. + assertTrue(lpEvent.getLp().getRoutes().size() >= 1); } finally { stopEthernetTethering(tetheringEventCallback); maybeCloseTestInterface(downstreamIface); diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index dc90d686f0..89fa34c60c 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -41,6 +41,7 @@ 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 com.android.networkstack.tethering.TetheringConfiguration.WIFIP2PGO_LOCAL_NETWORK_AGENT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -1123,20 +1124,33 @@ public class IpServerTest { @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT, enabled = false) + @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT) @Test public void testTetheringNetworkAgent_tetheringAgentDisabled() throws Exception { doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false); + // Even if the flag is enabled, no wifip2p network agent if the tethering network + // agent feature is not supported. + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, 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) + @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT, enabled = false) @Test public void testTetheringNetworkAgent_p2pGroupOwnerAgentDisabled() throws Exception { doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false); } + @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) + @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT) + @Test + public void testTetheringNetworkAgent_p2pGroupOwnerAgentEnabled() throws Exception { + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, true); + } + private void doTestTetheringNetworkAgent(int scope, boolean expectAgentEnabled) throws Exception { initStateMachine(TETHERING_USB); diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp index c94087e2e6..9e1878331e 100644 --- a/tests/cts/multidevices/snippet/Android.bp +++ b/tests/cts/multidevices/snippet/Android.bp @@ -16,6 +16,20 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +java_library { + name: "ConnectivityWifiP2pMultiDevicesSnippetLib", + srcs: [ + "Wifip2pMultiDevicesSnippet.kt", + ], + static_libs: [ + "mobly-snippet-lib", + "cts-net-utils", + ], + visibility: [ + "//packages/modules/Connectivity/tests/cts/tethering", + ], +} + android_test_helper_app { name: "connectivity_multi_devices_snippet", defaults: [ diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp index a1e0797490..38042851a3 100644 --- a/tests/cts/tethering/Android.bp +++ b/tests/cts/tethering/Android.bp @@ -30,9 +30,11 @@ java_defaults { srcs: [ "src/**/*.java", + "src/**/*.kt", ], static_libs: [ + "ConnectivityWifiP2pMultiDevicesSnippetLib", "TetheringCommonTests", "com.android.net.flags-aconfig-java", "compatibility-device-util-axt", diff --git a/tests/cts/tethering/AndroidManifest.xml b/tests/cts/tethering/AndroidManifest.xml index bad722bb0f..aede2d724e 100644 --- a/tests/cts/tethering/AndroidManifest.xml +++ b/tests/cts/tethering/AndroidManifest.xml @@ -20,6 +20,9 @@ + + diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringWifiP2pTest.kt b/tests/cts/tethering/src/android/tethering/cts/TetheringWifiP2pTest.kt new file mode 100644 index 0000000000..e7e9fdd9f2 --- /dev/null +++ b/tests/cts/tethering/src/android/tethering/cts/TetheringWifiP2pTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 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 android.tethering.cts + +import android.Manifest.permission.LOG_COMPAT_CHANGE +import android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG +import android.app.compat.CompatChanges +import android.content.pm.PackageManager +import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.connectivity.ConnectivityCompatChanges +import android.os.Build +import android.provider.DeviceConfig +import android.text.TextUtils +import androidx.test.platform.app.InstrumentationRegistry +import com.android.testutils.AutoReleaseNetworkCallbackRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.DeviceConfigRule +import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged +import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.runAsShell +import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet +import java.util.Random +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@RunWith(DevSdkIgnoreRunner::class) +class TetheringWifiP2pTest { + // For manipulating feature flag before and after testing. + @get:Rule + val deviceConfigRule = DeviceConfigRule() + + @get:Rule + val networkCallbackRule = AutoReleaseNetworkCallbackRule() + + companion object { + // Shamelessly copied from TetheringConfiguration. + private const val TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent" + private const val WIFIP2PGO_LOCAL_NETWORK_AGENT = "wifip2pgo_local_network_agent" + } + + private val context = InstrumentationRegistry.getInstrumentation().getTargetContext() + private val pm = context.packageManager!! + + private fun assumeMatchNonThreadLocalNetworksEnabled() { + assumeTrue(runAsShell(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE) { + CompatChanges.isChangeEnabled( + ConnectivityCompatChanges.ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS + ) + }) + } + + private fun getRandomString(): String { + return "%09d".format(Random().nextInt(1_000_000_000)) + } + + @Test + fun testWifiP2pGoNetworkAgent_networkCallbacks() { + // Check preconditions. + assumeMatchNonThreadLocalNetworksEnabled() + assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) + assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) + deviceConfigRule.setConfig( + DeviceConfig.NAMESPACE_TETHERING, + TETHERING_LOCAL_NETWORK_AGENT, + "1" + ) + deviceConfigRule.setConfig( + DeviceConfig.NAMESPACE_TETHERING, + WIFIP2PGO_LOCAL_NETWORK_AGENT, + "1" + ) + + val cb = TestableNetworkCallback() + val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK) + .addTransportType(TRANSPORT_WIFI).build() + networkCallbackRule.registerNetworkCallback(request, cb) + + val wifiP2pSnippet = Wifip2pMultiDevicesSnippet() + try { + wifiP2pSnippet.startWifiP2p() + wifiP2pSnippet.createGroup("DIRECT-" + getRandomString(), getRandomString()) + + val availableEvent = cb.expect() + // Verify Capabilities. + val capEvent = cb.eventuallyExpect() + assertEquals(availableEvent.network, capEvent.network) + assertTrue(capEvent.caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) + assertTrue(capEvent.caps.hasTransport(TRANSPORT_WIFI)) + + // Verify LinkProperties. + val lpEvent = cb.eventuallyExpect() + assertEquals(capEvent.network, lpEvent.network) + assertFalse(TextUtils.isEmpty(lpEvent.lp.interfaceName)) + // At least 1 Ipv4 address and route. + assertTrue(lpEvent.lp.hasIPv4Address()) + assertTrue(lpEvent.lp.routes.size >= 1) + } finally { + wifiP2pSnippet.stopWifiP2p() + } + } +}