Merge "Add a mode for cell radios unable to time share"
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 17fc3b4..ae5cfda 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -1055,6 +1055,14 @@
return isValidTransport(transportType) && ((mTransportTypes & (1 << transportType)) != 0);
}
+ /**
+ * Returns true iff this NetworkCapabilities has the specified transport and no other.
+ * @hide
+ */
+ public boolean hasSingleTransport(@Transport int transportType) {
+ return mTransportTypes == (1 << transportType);
+ }
+
private void combineTransportTypes(NetworkCapabilities nc) {
this.mTransportTypes |= nc.mTransportTypes;
}
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index b22457a..f8f86a2 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -125,4 +125,15 @@
details on what is happening. -->
<bool name="config_partialConnectivityNotifiedAsNoInternet">false</bool>
+ <!-- Whether the cell radio of the device is capable of timesharing.
+
+ Whether the cell radio is capable of timesharing between two different networks
+ even for a few seconds. When this is false, the networking stack will ask telephony
+ networks to disconnect immediately, instead of lingering, when outscored by some
+ other telephony network (typically on another subscription). This deprives apps
+ of a chance to gracefully migrate to the new network and degrades the experience
+ for apps, so it should only be set to false when timesharing on the cell radio has
+ extreme adverse effects on performance of the new network.
+ -->
+ <bool translatable="false" name="config_cellular_radio_timesharing_capable">true</bool>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 5af13d7..e5010d7 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -36,6 +36,7 @@
<item type="bool" name="config_partialConnectivityNotifiedAsNoInternet"/>
<item type="drawable" name="stat_notify_wifi_in_range"/>
<item type="drawable" name="stat_notify_rssi_in_range"/>
+ <item type="bool" name="config_cellular_radio_timesharing_capable" />
</policy>
</overlayable>
</resources>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 9f63191..c69af06 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -80,6 +80,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
+import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired;
@@ -335,6 +336,9 @@
protected int mLingerDelayMs; // Can't be final, or test subclass constructors can't change it.
@VisibleForTesting
protected int mNascentDelayMs;
+ // True if the cell radio of the device is capable of time-sharing.
+ @VisibleForTesting
+ protected boolean mCellularRadioTimesharingCapable = true;
// How long to delay to removal of a pending intent based request.
// See ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
@@ -1375,6 +1379,12 @@
NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL,
NetworkRequest.Type.BACKGROUND_REQUEST);
+ mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+ // TODO: Consider making the timer customizable.
+ mNascentDelayMs = DEFAULT_NASCENT_DELAY_MS;
+ mCellularRadioTimesharingCapable =
+ mResources.get().getBoolean(R.bool.config_cellular_radio_timesharing_capable);
+
mHandlerThread = mDeps.makeHandlerThread();
mHandlerThread.start();
mHandler = new InternalHandler(mHandlerThread.getLooper());
@@ -1385,10 +1395,6 @@
mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
- mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
- // TODO: Consider making the timer customizable.
- mNascentDelayMs = DEFAULT_NASCENT_DELAY_MS;
-
mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
mDnsResolver = Objects.requireNonNull(dnsresolver, "missing IDnsResolver");
@@ -7824,6 +7830,30 @@
bundle.putParcelable(t.getClass().getSimpleName(), t);
}
+ /**
+ * Returns whether reassigning a request from an NAI to another can be done gracefully.
+ *
+ * When a request should be assigned to a new network, it is normally lingered to give
+ * time for apps to gracefully migrate their connections. When both networks are on the same
+ * radio, but that radio can't do time-sharing efficiently, this may end up being
+ * counter-productive because any traffic on the old network may drastically reduce the
+ * performance of the new network.
+ * The stack supports a configuration to let modem vendors state that their radio can't
+ * do time-sharing efficiently. If this configuration is set, the stack assumes moving
+ * from one cell network to another can't be done gracefully.
+ *
+ * @param oldNai the old network serving the request
+ * @param newNai the new network serving the request
+ * @return whether the switch can be graceful
+ */
+ private boolean canSupportGracefulNetworkSwitch(@NonNull final NetworkAgentInfo oldSatisfier,
+ @NonNull final NetworkAgentInfo newSatisfier) {
+ if (mCellularRadioTimesharingCapable) return true;
+ return !oldSatisfier.networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)
+ || !newSatisfier.networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)
+ || !newSatisfier.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY);
+ }
+
private void teardownUnneededNetwork(NetworkAgentInfo nai) {
if (nai.numRequestNetworkRequests() != 0) {
for (int i = 0; i < nai.numNetworkRequests(); i++) {
@@ -8084,7 +8114,13 @@
log(" accepting network in place of " + previousSatisfier.toShortString());
}
previousSatisfier.removeRequest(previousRequest.requestId);
- previousSatisfier.lingerRequest(previousRequest.requestId, now);
+ if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)) {
+ // If this network switch can't be supported gracefully, the request is not
+ // lingered. This allows letting go of the network sooner to reclaim some
+ // performance on the new network, since the radio can't do both at the same
+ // time while preserving good performance.
+ previousSatisfier.lingerRequest(previousRequest.requestId, now);
+ }
} else {
if (VDBG || DDBG) log(" accepting network in place of null");
}
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index 970b7d2..4dc86ff 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -27,6 +27,7 @@
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
@@ -310,6 +311,10 @@
assertTrue(mDisconnected.block(timeoutMs));
}
+ public void assertNotDisconnected(long timeoutMs) {
+ assertFalse(mDisconnected.block(timeoutMs));
+ }
+
public void sendLinkProperties(LinkProperties lp) {
mNetworkAgent.sendLinkProperties(lp);
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 2aba0b4..1c7ac44 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -1792,6 +1792,8 @@
doReturn(R.integer.config_networkAvoidBadWifi).when(mResources)
.getIdentifier(eq("config_networkAvoidBadWifi"), eq("integer"), any());
doReturn(1).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
+ doReturn(true).when(mResources)
+ .getBoolean(R.bool.config_cellular_radio_timesharing_capable);
final ConnectivityResources connRes = mock(ConnectivityResources.class);
doReturn(mResources).when(connRes).get();
@@ -2273,8 +2275,22 @@
// After waitForIdle(), the message was processed and the service didn't crash.
}
+ // TODO : migrate to @Parameterized
@Test
- public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception {
+ public void testValidatedCellularOutscoresUnvalidatedWiFi_CanTimeShare() throws Exception {
+ // The behavior of this test should be the same whether the radio can time share or not.
+ doTestValidatedCellularOutscoresUnvalidatedWiFi(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testValidatedCellularOutscoresUnvalidatedWiFi_CannotTimeShare() throws Exception {
+ doTestValidatedCellularOutscoresUnvalidatedWiFi(false);
+ }
+
+ public void doTestValidatedCellularOutscoresUnvalidatedWiFi(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up unvalidated WiFi
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
ExpectedBroadcast b = registerConnectivityBroadcast(1);
@@ -2308,8 +2324,21 @@
verifyNoNetwork();
}
+ // TODO : migrate to @Parameterized
@Test
- public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception {
+ public void testUnvalidatedWifiOutscoresUnvalidatedCellular_CanTimeShare() throws Exception {
+ doTestUnvalidatedWifiOutscoresUnvalidatedCellular(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testUnvalidatedWifiOutscoresUnvalidatedCellular_CannotTimeShare() throws Exception {
+ doTestUnvalidatedWifiOutscoresUnvalidatedCellular(false);
+ }
+
+ public void doTestUnvalidatedWifiOutscoresUnvalidatedCellular(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up unvalidated cellular.
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
ExpectedBroadcast b = registerConnectivityBroadcast(1);
@@ -2334,8 +2363,21 @@
verifyNoNetwork();
}
+ // TODO : migrate to @Parameterized
@Test
- public void testUnlingeringDoesNotValidate() throws Exception {
+ public void testUnlingeringDoesNotValidate_CanTimeShare() throws Exception {
+ doTestUnlingeringDoesNotValidate(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testUnlingeringDoesNotValidate_CannotTimeShare() throws Exception {
+ doTestUnlingeringDoesNotValidate(false);
+ }
+
+ public void doTestUnlingeringDoesNotValidate(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up unvalidated WiFi.
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
ExpectedBroadcast b = registerConnectivityBroadcast(1);
@@ -2362,8 +2404,134 @@
NET_CAPABILITY_VALIDATED));
}
+ // TODO : migrate to @Parameterized
@Test
- public void testCellularOutscoresWeakWifi() throws Exception {
+ public void testRequestMigrationToSameTransport_CanTimeShare() throws Exception {
+ // Simulate a device where the cell radio is capable of time sharing
+ mService.mCellularRadioTimesharingCapable = true;
+ doTestRequestMigrationToSameTransport(TRANSPORT_CELLULAR, true);
+ doTestRequestMigrationToSameTransport(TRANSPORT_WIFI, true);
+ doTestRequestMigrationToSameTransport(TRANSPORT_ETHERNET, true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testRequestMigrationToSameTransport_CannotTimeShare() throws Exception {
+ // Simulate a device where the cell radio is not capable of time sharing
+ mService.mCellularRadioTimesharingCapable = false;
+ doTestRequestMigrationToSameTransport(TRANSPORT_CELLULAR, false);
+ doTestRequestMigrationToSameTransport(TRANSPORT_WIFI, true);
+ doTestRequestMigrationToSameTransport(TRANSPORT_ETHERNET, true);
+ }
+
+ public void doTestRequestMigrationToSameTransport(final int transport,
+ final boolean expectLingering) throws Exception {
+ // To speed up tests the linger delay is very short by default in tests but this
+ // test needs to make sure the delay is not incurred so a longer value is safer (it
+ // reduces the risk that a bug exists but goes undetected). The alarm manager in the test
+ // throws and crashes CS if this is set to anything more than the below constant though.
+ mService.mLingerDelayMs = UNREASONABLY_LONG_ALARM_WAIT_MS;
+
+ final TestNetworkCallback generalCb = new TestNetworkCallback();
+ final TestNetworkCallback defaultCb = new TestNetworkCallback();
+ mCm.registerNetworkCallback(
+ new NetworkRequest.Builder().addTransportType(transport | transport).build(),
+ generalCb);
+ mCm.registerDefaultNetworkCallback(defaultCb);
+
+ // Bring up net agent 1
+ final TestNetworkAgentWrapper net1 = new TestNetworkAgentWrapper(transport);
+ net1.connect(true);
+ // Make sure the default request is on net 1
+ generalCb.expectAvailableThenValidatedCallbacks(net1);
+ defaultCb.expectAvailableThenValidatedCallbacks(net1);
+
+ // Bring up net 2 with primary and mms
+ final TestNetworkAgentWrapper net2 = new TestNetworkAgentWrapper(transport);
+ net2.addCapability(NET_CAPABILITY_MMS);
+ net2.setScore(new NetworkScore.Builder().setTransportPrimary(true).build());
+ net2.connect(true);
+
+ // Make sure the default request goes to net 2
+ generalCb.expectAvailableCallbacksUnvalidated(net2);
+ if (expectLingering) {
+ generalCb.expectCallback(CallbackEntry.LOSING, net1);
+ }
+ generalCb.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, net2);
+ defaultCb.expectAvailableDoubleValidatedCallbacks(net2);
+
+ // Make sure cell 1 is unwanted immediately if the radio can't time share, but only
+ // after some delay if it can.
+ if (expectLingering) {
+ net1.assertNotDisconnected(TEST_CALLBACK_TIMEOUT_MS); // always incurs the timeout
+ generalCb.assertNoCallback();
+ // assertNotDisconnected waited for TEST_CALLBACK_TIMEOUT_MS, so waiting for the
+ // linger period gives TEST_CALLBACK_TIMEOUT_MS time for the event to process.
+ net1.expectDisconnected(UNREASONABLY_LONG_ALARM_WAIT_MS);
+ } else {
+ net1.expectDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+ }
+ net1.disconnect();
+ generalCb.expectCallback(CallbackEntry.LOST, net1);
+
+ // Remove primary from net 2
+ net2.setScore(new NetworkScore.Builder().build());
+ // Request MMS
+ final TestNetworkCallback mmsCallback = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(),
+ mmsCallback);
+ mmsCallback.expectAvailableCallbacksValidated(net2);
+
+ // Bring up net 3 with primary but without MMS
+ final TestNetworkAgentWrapper net3 = new TestNetworkAgentWrapper(transport);
+ net3.setScore(new NetworkScore.Builder().setTransportPrimary(true).build());
+ net3.connect(true);
+
+ // Make sure default goes to net 3, but the MMS request doesn't
+ generalCb.expectAvailableThenValidatedCallbacks(net3);
+ defaultCb.expectAvailableDoubleValidatedCallbacks(net3);
+ mmsCallback.assertNoCallback();
+ net2.assertNotDisconnected(TEST_CALLBACK_TIMEOUT_MS); // Always incurs the timeout
+
+ // Revoke MMS request and make sure net 2 is torn down with the appropriate delay
+ mCm.unregisterNetworkCallback(mmsCallback);
+ if (expectLingering) {
+ // If the radio can time share, the linger delay hasn't elapsed yet, so apps will
+ // get LOSING. If the radio can't time share, this is a hard loss, since the last
+ // request keeping up this network has been removed and the network isn't lingering
+ // for any other request.
+ generalCb.expectCallback(CallbackEntry.LOSING, net2);
+ net2.assertNotDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+ generalCb.assertNoCallback();
+ net2.expectDisconnected(UNREASONABLY_LONG_ALARM_WAIT_MS);
+ } else {
+ net2.expectDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+ }
+ net2.disconnect();
+ generalCb.expectCallback(CallbackEntry.LOST, net2);
+ defaultCb.assertNoCallback();
+
+ net3.disconnect();
+ mCm.unregisterNetworkCallback(defaultCb);
+ mCm.unregisterNetworkCallback(generalCb);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testCellularOutscoresWeakWifi_CanTimeShare() throws Exception {
+ // The behavior of this test should be the same whether the radio can time share or not.
+ doTestCellularOutscoresWeakWifi(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testCellularOutscoresWeakWifi_CannotTimeShare() throws Exception {
+ doTestCellularOutscoresWeakWifi(false);
+ }
+
+ public void doTestCellularOutscoresWeakWifi(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up validated cellular.
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
ExpectedBroadcast b = registerConnectivityBroadcast(1);
@@ -2388,8 +2556,21 @@
verifyActiveNetwork(TRANSPORT_WIFI);
}
+ // TODO : migrate to @Parameterized
@Test
- public void testReapingNetwork() throws Exception {
+ public void testReapingNetwork_CanTimeShare() throws Exception {
+ doTestReapingNetwork(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testReapingNetwork_CannotTimeShare() throws Exception {
+ doTestReapingNetwork(false);
+ }
+
+ public void doTestReapingNetwork(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up WiFi without NET_CAPABILITY_INTERNET.
// Expect it to be torn down immediately because it satisfies no requests.
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -2417,8 +2598,21 @@
mWiFiNetworkAgent.expectDisconnected();
}
+ // TODO : migrate to @Parameterized
@Test
- public void testCellularFallback() throws Exception {
+ public void testCellularFallback_CanTimeShare() throws Exception {
+ doTestCellularFallback(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testCellularFallback_CannotTimeShare() throws Exception {
+ doTestCellularFallback(false);
+ }
+
+ public void doTestCellularFallback(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up validated cellular.
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
ExpectedBroadcast b = registerConnectivityBroadcast(1);
@@ -2455,8 +2649,21 @@
verifyActiveNetwork(TRANSPORT_WIFI);
}
+ // TODO : migrate to @Parameterized
@Test
- public void testWiFiFallback() throws Exception {
+ public void testWiFiFallback_CanTimeShare() throws Exception {
+ doTestWiFiFallback(true);
+ }
+
+ // TODO : migrate to @Parameterized
+ @Test
+ public void testWiFiFallback_CannotTimeShare() throws Exception {
+ doTestWiFiFallback(false);
+ }
+
+ public void doTestWiFiFallback(
+ final boolean cellRadioTimesharingCapable) throws Exception {
+ mService.mCellularRadioTimesharingCapable = cellRadioTimesharingCapable;
// Test bringing up unvalidated WiFi.
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
ExpectedBroadcast b = registerConnectivityBroadcast(1);