diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index d2dbbfd499..9cba46e8be 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -507,6 +507,9 @@ public class ConnectivityService extends IConnectivityManager.Stub private static final int ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS = 20 * 1000; // Timeout in case the "actively prefer bad wifi" feature is off private static final int DEFAULT_EVALUATION_TIMEOUT_MS = 8 * 1000; + // Timeout threshold on how long a network is allowed to stay at SUSPENDED state. When the + // timeout is reached, the network will be torn down. + private static final int DEFAULT_NETWORK_SUSPENDED_TIMEOUT_MS = 5 * 60 * 1000; // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing. private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger"; @@ -978,6 +981,12 @@ public class ConnectivityService extends IConnectivityManager.Stub */ private static final int EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS = 63; + /** + * indicates a timeout period to wait for the network leaving suspended state is over. + * The network should be torn down now. + */ + private static final int EVENT_TIMEOUT_NETWORK_SUSPENDED = 64; + /** * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification * should be shown. @@ -1760,6 +1769,21 @@ public class ConnectivityService extends IConnectivityManager.Stub WIFI_DATA_INACTIVITY_TIMEOUT, 15); } + /** Returns the timeout duration for a network to stay in SUSPENDED state before being torn + * down. + */ + public int getNetworkSuspendedTimeoutMs() { + return DEFAULT_NETWORK_SUSPENDED_TIMEOUT_MS; + } + + /** + * Whether the enforcement of short network suspension duration should be enabled. + */ + public boolean isShortNetworkSuspensionEnforced(Context context) { + // The feature is only enabled on watch for now. See b/422119228 for details. + return context.getPackageManager().hasSystemFeature(FEATURE_WATCH); + } + /** * @see DeviceConfigUtils#isTetheringFeatureEnabled */ @@ -3757,6 +3781,11 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + private void handleNetwokSuspendedTimeout(NetworkAgentInfo nai) { + // Tear down the network if it has stayed in SUSPENDED state for longer than intended. + disconnectAndDestroyNetwork(nai); + } + private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) { if (!mQueueCallbacksForFrozenApps) { return; @@ -5833,6 +5862,12 @@ public class ConnectivityService extends IConnectivityManager.Stub } nai.networkMonitor().notifyNetworkDisconnected(); mNetworkAgentInfos.remove(nai); + if (mDeps.isShortNetworkSuspensionEnforced(mContext)) { + // Remove the message to disconnect the network if it's in suspended state for too long, + // this is to allow the system to reclaim the memory associated with this NAI earlier, + // rather than unnecessarily waiting for the timeout period. + mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_SUSPENDED, nai); + } nai.clatd.update(); synchronized (mNetworkForNetId) { // Remove the NetworkAgent, but don't mark the netId as @@ -7347,6 +7382,9 @@ public class ConnectivityService extends IConnectivityManager.Stub case EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS: handleClearFirewallDestroySocketReasons(msg.arg1); break; + case EVENT_TIMEOUT_NETWORK_SUSPENDED: + handleNetwokSuspendedTimeout((NetworkAgentInfo) msg.obj); + break; } } } @@ -10912,6 +10950,15 @@ public class ConnectivityService extends IConnectivityManager.Stub // TODO (b/73132094) : remove this call once the few users of onSuspended and // onResumed have been removed. notifyNetworkCallbacks(nai, suspended ? CALLBACK_SUSPENDED : CALLBACK_RESUMED); + if (mDeps.isShortNetworkSuspensionEnforced(mContext)) { + if (suspended) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_SUSPENDED, + nai), mDeps.getNetworkSuspendedTimeoutMs()); + } else { + mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_SUSPENDED, nai); + } + } } if (prevSuspended != suspended || prevRoaming != roaming) { // updateNetworkInfo will mix in the suspended info from the capabilities and diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkSuspendedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkSuspendedTest.kt new file mode 100644 index 0000000000..1cdfc0c396 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkSuspendedTest.kt @@ -0,0 +1,100 @@ +/* + * 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 com.android.server + +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkRequest +import android.os.Build +import com.android.testutils.ConnectivityModuleTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.TestableNetworkCallback.Event.Lost +import kotlin.test.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith + +private const val NETWORK_SUSPENDED_TIMEOUT_SHORT = 150 + +@DevSdkIgnoreRunner.MonitorThreadLeak +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S) +class CSNetworkSuspendedTest : CSTest() { + private val cb = TestableNetworkCallback(timeoutMs = 30 * 1000) + private val cellNc = defaultNc().addTransportType(TRANSPORT_CELLULAR).addCapability( + NET_CAPABILITY_INTERNET + ) + private val cellNcSuspended = defaultNc().removeCapability( + NET_CAPABILITY_NOT_SUSPENDED + ).addCapability( + NET_CAPABILITY_INTERNET + ).addTransportType(TRANSPORT_CELLULAR) + + @ConnectivityModuleTest + @Test + fun testNetworkSuspendedTimeoutDisconnected() { + // Set a short timeout to make the test faster. + deps.networkSuspendedTimeoutForTestMs = NETWORK_SUSPENDED_TIMEOUT_SHORT + val agent = Agent(cellNc) + agent.connect() + val networkInfo = cm.activeNetworkInfo + assertNotNull(networkInfo) + cm.registerNetworkCallback( + NetworkRequest.Builder().setCapabilities( + cellNc + ).removeCapability(NET_CAPABILITY_NOT_SUSPENDED).build(), + cb + ) + // Update the network to suspended. + agent.sendNetworkCapabilities(cellNcSuspended) + + // Verify that the suspended network eventually disconnects. + cb.assertNoCallback( + NETWORK_SUSPENDED_TIMEOUT_SHORT.toLong() + ) { it is Lost && agent.network == it.network } + cb.eventuallyExpect { it.network == agent.network } + cm.unregisterNetworkCallback(cb) + } + + @ConnectivityModuleTest + @Test + fun testNetworkExitsSuspendedRemainsConnected() { + // Set a slightly longer(0.5s) timeout to test (SUSPENDED -> NOT_SUSPENDED) + deps.networkSuspendedTimeoutForTestMs = 500 + val agent = Agent(cellNc) + agent.connect() + val networkInfo = cm.activeNetworkInfo + assertNotNull(networkInfo) + cm.registerNetworkCallback( + NetworkRequest.Builder().setCapabilities( + cellNc + ).removeCapability(NET_CAPABILITY_NOT_SUSPENDED).build(), + cb + ) + // Update the network to suspended. + agent.sendNetworkCapabilities(cellNcSuspended) + // Update the network back to not suspended. + agent.sendNetworkCapabilities(cellNc) + + // Verify that the network is not disconnected after the suspended state timeout, + // since the network returned to NOT_SUSPENDED state before the timeout. + cb.assertNoCallback(750) { it is Lost && agent.network == it.network } + cm.unregisterNetworkCallback(cb) + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt index 3a36ddc277..7f4009f06f 100644 --- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -423,6 +423,16 @@ open class CSTest { return defaultWifiDataInactivityTimeoutForTest } + var networkSuspendedTimeoutForTestMs: Int = 10 + override fun getNetworkSuspendedTimeoutMs(): Int { + return networkSuspendedTimeoutForTestMs + } + + // Enable the feature for testing. + override fun isShortNetworkSuspensionEnforced(context: Context?): Boolean { + return true + } + override fun isChangeEnabled(changeId: Long, pkg: String, user: UserHandle) = changeId in enabledChangeIds override fun isChangeEnabled(changeId: Long, uid: Int) =