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()
+ }
+ }
+}