Disconnect a network if it's suspended for 5 minutes for wear

The suspension state is designed to be a transient state. Implement an
enforcement to disconnect the network after staying at this state for 5
minutes. This can avoid apps unnecessarily waking up the device for
network activities even when there's no network actually connected.

Test: unit tests. Locally tested with a signal blocking bag.
Bug: 404570281
Flag: EXEMPT mainline
Change-Id: I1427a72402f7ec64eda537a89c86768466a72328
This commit is contained in:
Hongyi Zhang 2025-05-29 00:31:08 +00:00
commit 3dcb3c5890
3 changed files with 157 additions and 0 deletions

View file

@ -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

View file

@ -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<Lost> { 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)
}
}

View file

@ -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) =