Merge "Add a NetworkAgent API to indicate that a network will be replaced."

This commit is contained in:
Lorenzo Colitti
2022-03-15 04:44:16 +00:00
committed by Gerrit Code Review
7 changed files with 329 additions and 28 deletions

View File

@@ -236,6 +236,7 @@ package android.net {
public abstract class NetworkAgent { public abstract class NetworkAgent {
ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider); ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider); ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
method public void destroyAndAwaitReplacement(@IntRange(from=0, to=0x1388) int);
method @Nullable public android.net.Network getNetwork(); method @Nullable public android.net.Network getNetwork();
method public void markConnected(); method public void markConnected();
method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData); method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);

View File

@@ -47,4 +47,5 @@ oneway interface INetworkAgentRegistry {
void sendAddDscpPolicy(in DscpPolicy policy); void sendAddDscpPolicy(in DscpPolicy policy);
void sendRemoveDscpPolicy(int policyId); void sendRemoveDscpPolicy(int policyId);
void sendRemoveAllDscpPolicies(); void sendRemoveAllDscpPolicies();
void sendDestroyAndAwaitReplacement(int timeoutMillis);
} }

View File

@@ -434,6 +434,14 @@ public abstract class NetworkAgent {
*/ */
public static final int CMD_DSCP_POLICY_STATUS = BASE + 28; public static final int CMD_DSCP_POLICY_STATUS = BASE + 28;
/**
* Sent by the NetworkAgent to ConnectivityService to notify that this network is expected to be
* replaced within the specified time by a similar network.
* arg1 = timeout in milliseconds
* @hide
*/
public static final int EVENT_DESTROY_AND_AWAIT_REPLACEMENT = BASE + 29;
private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) { private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType, final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
config.legacyTypeName, config.legacySubTypeName); config.legacyTypeName, config.legacySubTypeName);
@@ -942,6 +950,45 @@ public abstract class NetworkAgent {
queueOrSendMessage(reg -> reg.sendTeardownDelayMs(teardownDelayMillis)); queueOrSendMessage(reg -> reg.sendTeardownDelayMs(teardownDelayMillis));
} }
/**
* Indicates that this agent will likely soon be replaced by another agent for a very similar
* network (e.g., same Wi-Fi SSID).
*
* If the network is not currently satisfying any {@link NetworkRequest}s, it will be torn down.
* If it is satisfying requests, then the native network corresponding to the agent will be
* destroyed immediately, but the agent will remain registered and will continue to satisfy
* requests until {@link #unregister} is called, the network is replaced by an equivalent or
* better network, or the specified timeout expires. During this time:
*
* <ul>
* <li>The agent may not send any further updates, for example by calling methods
* such as {@link #sendNetworkCapabilities}, {@link #sendLinkProperties},
* {@link #sendNetworkScore(NetworkScore)} and so on. Any such updates will be ignored.
* <li>The network will remain connected and continue to satisfy any requests that it would
* otherwise satisfy (including, possibly, the default request).
* <li>The validation state of the network will not change, and calls to
* {@link ConnectivityManager#reportNetworkConnectivity(Network, boolean)} will be ignored.
* </ul>
*
* Once this method is called, it is not possible to restore the agent to a functioning state.
* If a replacement network becomes available, then a new agent must be registered. When that
* replacement network is fully capable of replacing this network (including, possibly, being
* validated), this agent will no longer be needed and will be torn down. Otherwise, this agent
* can be disconnected by calling {@link #unregister}. If {@link #unregister} is not called,
* this agent will automatically be unregistered when the specified timeout expires. Any
* teardown delay previously set using{@link #setTeardownDelayMillis} is ignored.
*
* <p>This method has no effect if {@link #markConnected} has not yet been called.
* <p>This method may only be called once.
*
* @param timeoutMillis the timeout after which this network will be unregistered even if
* {@link #unregister} was not called.
*/
public void destroyAndAwaitReplacement(
@IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int timeoutMillis) {
queueOrSendMessage(reg -> reg.sendDestroyAndAwaitReplacement(timeoutMillis));
}
/** /**
* Change the legacy subtype of this network agent. * Change the legacy subtype of this network agent.
* *

View File

@@ -3503,6 +3503,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
return false; return false;
} }
private boolean isDisconnectRequest(Message msg) {
if (msg.what != NetworkAgent.EVENT_NETWORK_INFO_CHANGED) return false;
final NetworkInfo info = (NetworkInfo) ((Pair) msg.obj).second;
return info.getState() == NetworkInfo.State.DISCONNECTED;
}
// must be stateless - things change under us. // must be stateless - things change under us.
private class NetworkStateTrackerHandler extends Handler { private class NetworkStateTrackerHandler extends Handler {
public NetworkStateTrackerHandler(Looper looper) { public NetworkStateTrackerHandler(Looper looper) {
@@ -3519,6 +3525,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
return; return;
} }
// If the network has been destroyed, the only thing that it can do is disconnect.
if (nai.destroyed && !isDisconnectRequest(msg)) {
return;
}
switch (msg.what) { switch (msg.what) {
case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: { case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
final NetworkCapabilities networkCapabilities = new NetworkCapabilities( final NetworkCapabilities networkCapabilities = new NetworkCapabilities(
@@ -3620,12 +3631,60 @@ public class ConnectivityService extends IConnectivityManager.Stub
} }
break; break;
} }
case NetworkAgent.EVENT_DESTROY_AND_AWAIT_REPLACEMENT: {
// If nai is not yet created, or is already destroyed, ignore.
if (!shouldDestroyNativeNetwork(nai)) break;
final int timeoutMs = (int) arg.second;
if (timeoutMs < 0 || timeoutMs > NetworkAgent.MAX_TEARDOWN_DELAY_MS) {
Log.e(TAG, "Invalid network replacement timer " + timeoutMs
+ ", must be between 0 and " + NetworkAgent.MAX_TEARDOWN_DELAY_MS);
}
// Marking a network awaiting replacement is used to ensure that any requests
// satisfied by the network do not switch to another network until a
// replacement is available or the wait for a replacement times out.
// If the network is inactive (i.e., nascent or lingering), then there are no
// such requests, and there is no point keeping it. Just tear it down.
// Note that setLingerDuration(0) cannot be used to do this because the network
// could be nascent.
nai.clearInactivityState();
if (unneeded(nai, UnneededFor.TEARDOWN)) {
Log.d(TAG, nai.toShortString()
+ " marked awaiting replacement is unneeded, tearing down instead");
teardownUnneededNetwork(nai);
break;
}
Log.d(TAG, "Marking " + nai.toShortString()
+ " destroyed, awaiting replacement within " + timeoutMs + "ms");
destroyNativeNetwork(nai);
// TODO: deduplicate this call with the one in disconnectAndDestroyNetwork.
// This is not trivial because KeepaliveTracker#handleStartKeepalive does not
// consider the fact that the network could already have disconnected or been
// destroyed. Fix the code to send ERROR_INVALID_NETWORK when this happens
// (taking care to ensure no dup'd FD leaks), then remove the code duplication
// and move this code to a sensible location (destroyNativeNetwork perhaps?).
mKeepaliveTracker.handleStopAllKeepalives(nai,
SocketKeepalive.ERROR_INVALID_NETWORK);
nai.updateScoreForNetworkAgentUpdate();
// This rematch is almost certainly not going to result in any changes, because
// the destroyed flag is only just above the "current satisfier wins"
// tie-breaker. But technically anything that affects scoring should rematch.
rematchAllNetworksAndRequests();
mHandler.postDelayed(() -> nai.disconnect(), timeoutMs);
break;
}
} }
} }
private boolean maybeHandleNetworkMonitorMessage(Message msg) { private boolean maybeHandleNetworkMonitorMessage(Message msg) {
final int netId = msg.arg2; final int netId = msg.arg2;
final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId); final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
// If a network has already been destroyed, all NetworkMonitor updates are ignored.
if (nai != null && nai.destroyed) return true;
switch (msg.what) { switch (msg.what) {
default: default:
return false; return false;
@@ -4125,6 +4184,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
} }
} }
private static boolean shouldDestroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
return nai.created && !nai.destroyed;
}
private void handleNetworkAgentDisconnected(Message msg) { private void handleNetworkAgentDisconnected(Message msg) {
NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj; NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
disconnectAndDestroyNetwork(nai); disconnectAndDestroyNetwork(nai);
@@ -4231,7 +4294,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
} }
private void destroyNetwork(NetworkAgentInfo nai) { private void destroyNetwork(NetworkAgentInfo nai) {
if (nai.created) { if (shouldDestroyNativeNetwork(nai)) {
// Tell netd to clean up the configuration for this network // Tell netd to clean up the configuration for this network
// (routing rules, DNS, etc). // (routing rules, DNS, etc).
// This may be slow as it requires a lot of netd shelling out to ip and // This may be slow as it requires a lot of netd shelling out to ip and
@@ -4240,15 +4303,15 @@ public class ConnectivityService extends IConnectivityManager.Stub
// network or service a new request from an app), so network traffic isn't interrupted // network or service a new request from an app), so network traffic isn't interrupted
// for an unnecessarily long time. // for an unnecessarily long time.
destroyNativeNetwork(nai); destroyNativeNetwork(nai);
mDnsManager.removeNetwork(nai.network);
// clean up tc police filters on interface.
if (nai.everConnected && canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
} }
if (!nai.created && !SdkLevel.isAtLeastT()) {
// Backwards compatibility: send onNetworkDestroyed even if network was never created.
// This can never run if the code above runs because shouldDestroyNativeNetwork is
// false if the network was never created.
// TODO: delete when S is no longer supported.
nai.onNetworkDestroyed();
} }
mNetIdManager.releaseNetId(nai.network.getNetId()); mNetIdManager.releaseNetId(nai.network.getNetId());
nai.onNetworkDestroyed();
} }
private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) { private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
@@ -4291,6 +4354,18 @@ public class ConnectivityService extends IConnectivityManager.Stub
} catch (RemoteException | ServiceSpecificException e) { } catch (RemoteException | ServiceSpecificException e) {
loge("Exception destroying network: " + e); loge("Exception destroying network: " + e);
} }
// TODO: defer calling this until the network is removed from mNetworkAgentInfos.
// Otherwise, a private DNS configuration update for a destroyed network, or one that never
// gets created, could add data to DnsManager data structures that will never get deleted.
mDnsManager.removeNetwork(nai.network);
// clean up tc police filters on interface.
if (nai.everConnected && canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
}
nai.destroyed = true;
nai.onNetworkDestroyed();
} }
// If this method proves to be too slow then we can maintain a separate // If this method proves to be too slow then we can maintain a separate
@@ -8543,11 +8618,19 @@ public class ConnectivityService extends IConnectivityManager.Stub
log(" accepting network in place of " + previousSatisfier.toShortString()); log(" accepting network in place of " + previousSatisfier.toShortString());
} }
previousSatisfier.removeRequest(previousRequest.requestId); previousSatisfier.removeRequest(previousRequest.requestId);
if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)) { if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
&& !previousSatisfier.destroyed) {
// If this network switch can't be supported gracefully, the request is not // 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 // 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 // performance on the new network, since the radio can't do both at the same
// time while preserving good performance. // time while preserving good performance.
//
// Also don't linger the request if the old network has been destroyed.
// A destroyed network does not provide actual network connectivity, so
// lingering it is not useful. In particular this ensures that a destroyed
// network is outscored by its replacement,
// then it is torn down immediately instead of being lingered, and any apps that
// were using it immediately get onLost and can connect using the new network.
previousSatisfier.lingerRequest(previousRequest.requestId, now); previousSatisfier.lingerRequest(previousRequest.requestId, now);
} }
} else { } else {

View File

@@ -132,7 +132,7 @@ public class Nat464Xlat {
final boolean skip464xlat = (nai.netAgentConfig() != null) final boolean skip464xlat = (nai.netAgentConfig() != null)
&& nai.netAgentConfig().skip464xlat; && nai.netAgentConfig().skip464xlat;
return supported && connected && isIpv6OnlyNetwork && !skip464xlat return supported && connected && isIpv6OnlyNetwork && !skip464xlat && !nai.destroyed
&& (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR) && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
? isCellular464XlatEnabled() : true); ? isCellular464XlatEnabled() : true);
} }

View File

@@ -732,6 +732,12 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRa
mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES, mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES,
new Pair<>(NetworkAgentInfo.this, null)).sendToTarget(); new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
} }
@Override
public void sendDestroyAndAwaitReplacement(final int timeoutMillis) {
mHandler.obtainMessage(NetworkAgent.EVENT_DESTROY_AND_AWAIT_REPLACEMENT,
new Pair<>(NetworkAgentInfo.this, timeoutMillis)).sendToTarget();
}
} }
/** /**
@@ -976,7 +982,7 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRa
/** /**
* Update the ConnectivityService-managed bits in the score. * Update the ConnectivityService-managed bits in the score.
* *
* Call this after updating the network agent config. * Call this after changing any data that might affect the score (e.g., agent config).
*/ */
public void updateScoreForNetworkAgentUpdate() { public void updateScoreForNetworkAgentUpdate() {
mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
@@ -1256,6 +1262,8 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRa
+ "network{" + network + "} handle{" + network.getNetworkHandle() + "} ni{" + "network{" + network + "} handle{" + network.getNetworkHandle() + "} ni{"
+ networkInfo.toShortString() + "} " + networkInfo.toShortString() + "} "
+ mScore + " " + mScore + " "
+ (created ? " created" : "")
+ (destroyed ? " destroyed" : "")
+ (isNascent() ? " nascent" : (isLingering() ? " lingering" : "")) + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
+ (everValidated ? " everValidated" : "") + (everValidated ? " everValidated" : "")
+ (lastValidated ? " lastValidated" : "") + (lastValidated ? " lastValidated" : "")

View File

@@ -19,6 +19,7 @@ import android.Manifest.permission.NETWORK_SETTINGS
import android.app.Instrumentation import android.app.Instrumentation
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.EthernetNetworkSpecifier
import android.net.INetworkAgent import android.net.INetworkAgent
import android.net.INetworkAgentRegistry import android.net.INetworkAgentRegistry
import android.net.InetAddresses import android.net.InetAddresses
@@ -35,6 +36,7 @@ import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
@@ -42,7 +44,9 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_TEST import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_VPN import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkInfo import android.net.NetworkInfo
import android.net.NetworkProvider import android.net.NetworkProvider
@@ -100,6 +104,7 @@ import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeep
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
import com.android.testutils.TestableNetworkCallback import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertThrows
import org.junit.After import org.junit.After
import org.junit.Assume.assumeFalse import org.junit.Assume.assumeFalse
import org.junit.Before import org.junit.Before
@@ -112,6 +117,8 @@ import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.timeout import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import java.io.IOException
import java.net.DatagramSocket
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
@@ -249,14 +256,10 @@ class NetworkAgentTest {
.build() .build()
} }
private fun createNetworkAgent( private fun makeTestNetworkCapabilities(
context: Context = realContext,
specifier: String? = null, specifier: String? = null,
initialNc: NetworkCapabilities? = null, transports: IntArray = intArrayOf()
initialLp: LinkProperties? = null, ) = NetworkCapabilities().apply {
initialConfig: NetworkAgentConfig? = null
): TestableNetworkAgent {
val nc = initialNc ?: NetworkCapabilities().apply {
addTransportType(TRANSPORT_TEST) addTransportType(TRANSPORT_TEST)
removeCapability(NET_CAPABILITY_TRUSTED) removeCapability(NET_CAPABILITY_TRUSTED)
removeCapability(NET_CAPABILITY_INTERNET) removeCapability(NET_CAPABILITY_INTERNET)
@@ -269,7 +272,20 @@ class NetworkAgentTest {
if (null != specifier) { if (null != specifier) {
setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier)) setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
} }
for (t in transports) { addTransportType(t) }
// Most transports are not allowed on test networks unless the network is marked restricted.
// This test does not need
if (transports.size > 0) removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
} }
private fun createNetworkAgent(
context: Context = realContext,
specifier: String? = null,
initialNc: NetworkCapabilities? = null,
initialLp: LinkProperties? = null,
initialConfig: NetworkAgentConfig? = null
): TestableNetworkAgent {
val nc = initialNc ?: makeTestNetworkCapabilities(specifier)
val lp = initialLp ?: LinkProperties().apply { val lp = initialLp ?: LinkProperties().apply {
addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32)) addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null)) addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
@@ -284,12 +300,14 @@ class NetworkAgentTest {
context: Context = realContext, context: Context = realContext,
specifier: String? = UUID.randomUUID().toString(), specifier: String? = UUID.randomUUID().toString(),
initialConfig: NetworkAgentConfig? = null, initialConfig: NetworkAgentConfig? = null,
expectedInitSignalStrengthThresholds: IntArray? = intArrayOf() expectedInitSignalStrengthThresholds: IntArray? = intArrayOf(),
transports: IntArray = intArrayOf()
): Pair<TestableNetworkAgent, TestableNetworkCallback> { ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
val callback = TestableNetworkCallback() val callback = TestableNetworkCallback()
// Ensure this NetworkAgent is never unneeded by filing a request with its specifier. // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
requestNetwork(makeTestNetworkRequest(specifier = specifier), callback) requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
val agent = createNetworkAgent(context, specifier, initialConfig = initialConfig) val nc = makeTestNetworkCapabilities(specifier, transports)
val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
agent.setTeardownDelayMillis(0) agent.setTeardownDelayMillis(0)
// Connect the agent and verify initial status callbacks. // Connect the agent and verify initial status callbacks.
agent.register() agent.register()
@@ -301,6 +319,15 @@ class NetworkAgentTest {
return agent to callback return agent to callback
} }
private fun connectNetwork(vararg transports: Int): Pair<TestableNetworkAgent, Network> {
val (agent, callback) = createConnectedNetworkAgent(transports = transports)
val network = agent.network!!
// createConnectedNetworkAgent internally files a request; release it so that the network
// will be torn down if unneeded.
mCM.unregisterNetworkCallback(callback)
return agent to network
}
private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also { private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also {
mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID))) mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
} }
@@ -1123,4 +1150,138 @@ class NetworkAgentTest {
remoteAddresses remoteAddresses
) )
} }
@Test
fun testDestroyAndAwaitReplacement() {
// Keeps an eye on all test networks.
val matchAllCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
registerNetworkCallback(makeTestNetworkRequest(), matchAllCallback)
// File a request that matches and keeps up the best-scoring test network.
val testCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
requestNetwork(makeTestNetworkRequest(), testCallback)
// Connect the first network. This should satisfy the request.
val (agent1, network1) = connectNetwork()
matchAllCallback.expectAvailableThenValidatedCallbacks(network1)
testCallback.expectAvailableThenValidatedCallbacks(network1)
// Check that network1 exists by binding a socket to it and getting no exceptions.
network1.bindSocket(DatagramSocket())
// Connect a second agent. network1 is preferred because it was already registered, so
// testCallback will not see any events. agent2 is be torn down because it has no requests.
val (agent2, network2) = connectNetwork()
matchAllCallback.expectAvailableThenValidatedCallbacks(network2)
matchAllCallback.expectCallback<Lost>(network2)
agent2.expectCallback<OnNetworkUnwanted>()
agent2.expectCallback<OnNetworkDestroyed>()
assertNull(mCM.getLinkProperties(network2))
// Mark the first network as awaiting replacement. This should destroy the underlying
// native network and send onNetworkDestroyed, but will not send any NetworkCallbacks,
// because for callback and scoring purposes network1 is still connected.
agent1.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
agent1.expectCallback<OnNetworkDestroyed>()
assertThrows(IOException::class.java) { network1.bindSocket(DatagramSocket()) }
assertNotNull(mCM.getLinkProperties(network1))
// Calling destroyAndAwaitReplacement more than once has no effect.
// If it did, this test would fail because the 1ms timeout means that the network would be
// torn down before the replacement arrives.
agent1.destroyAndAwaitReplacement(1 /* timeoutMillis */)
// Connect a third network. Because network1 is awaiting replacement, network3 is preferred
// as soon as it validates (until then, it is outscored by network1).
// The fact that the first events seen by matchAllCallback is the connection of network3
// implicitly ensures that no callbacks are sent since network1 was lost.
val (agent3, network3) = connectNetwork()
matchAllCallback.expectAvailableThenValidatedCallbacks(network3)
testCallback.expectAvailableDoubleValidatedCallbacks(network3)
// As soon as the replacement arrives, network1 is disconnected.
// Check that this happens before the replacement timeout (5 seconds) fires.
matchAllCallback.expectCallback<Lost>(network1, 2_000 /* timeoutMs */)
agent1.expectCallback<OnNetworkUnwanted>()
// Test lingering:
// - Connect a higher-scoring network and check that network3 starts lingering.
// - Mark network3 awaiting replacement.
// - Check that network3 is torn down immediately without waiting for the linger timer or
// the replacement timer to fire. This is a regular teardown, so it results in
// onNetworkUnwanted before onNetworkDestroyed.
val (agent4, agent4callback) = createConnectedNetworkAgent()
val network4 = agent4.network!!
matchAllCallback.expectAvailableThenValidatedCallbacks(network4)
agent4.sendNetworkScore(NetworkScore.Builder().setTransportPrimary(true).build())
matchAllCallback.expectCallback<Losing>(network3)
testCallback.expectAvailableCallbacks(network4, validated = true)
mCM.unregisterNetworkCallback(agent4callback)
agent3.destroyAndAwaitReplacement(5_000)
agent3.expectCallback<OnNetworkUnwanted>()
matchAllCallback.expectCallback<Lost>(network3, 1000L)
agent3.expectCallback<OnNetworkDestroyed>()
// Now mark network4 awaiting replacement with a low timeout, and check that if no
// replacement arrives, it is torn down.
agent4.destroyAndAwaitReplacement(100 /* timeoutMillis */)
matchAllCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
testCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
agent4.expectCallback<OnNetworkDestroyed>()
agent4.expectCallback<OnNetworkUnwanted>()
// If a network that is awaiting replacement is unregistered, it disconnects immediately,
// before the replacement timeout fires.
val (agent5, network5) = connectNetwork()
matchAllCallback.expectAvailableThenValidatedCallbacks(network5)
testCallback.expectAvailableThenValidatedCallbacks(network5)
agent5.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
agent5.unregister()
matchAllCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
testCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
agent5.expectCallback<OnNetworkDestroyed>()
agent5.expectCallback<OnNetworkUnwanted>()
// If wifi is replaced within the timeout, the device does not switch to cellular.
val (cellAgent, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
testCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
matchAllCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
val (wifiAgent, wifiNetwork) = connectNetwork(TRANSPORT_WIFI)
testCallback.expectAvailableCallbacks(wifiNetwork, validated = true)
testCallback.expectCapabilitiesThat(wifiNetwork) {
it.hasCapability(NET_CAPABILITY_VALIDATED)
}
matchAllCallback.expectAvailableCallbacks(wifiNetwork, validated = false)
matchAllCallback.expectCallback<Losing>(cellNetwork)
matchAllCallback.expectCapabilitiesThat(wifiNetwork) {
it.hasCapability(NET_CAPABILITY_VALIDATED)
}
wifiAgent.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
wifiAgent.expectCallback<OnNetworkDestroyed>()
// Once the network is awaiting replacement, changing LinkProperties, NetworkCapabilities or
// score, or calling reportNetworkConnectivity, have no effect.
val wifiSpecifier = mCM.getNetworkCapabilities(wifiNetwork)!!.networkSpecifier
assertNotNull(wifiSpecifier)
assertTrue(wifiSpecifier is EthernetNetworkSpecifier)
val wifiNc = makeTestNetworkCapabilities(wifiSpecifier.interfaceName,
intArrayOf(TRANSPORT_WIFI))
wifiAgent.sendNetworkCapabilities(wifiNc)
val wifiLp = mCM.getLinkProperties(wifiNetwork)!!
val newRoute = RouteInfo(IpPrefix("192.0.2.42/24"))
assertFalse(wifiLp.getRoutes().contains(newRoute))
wifiLp.addRoute(newRoute)
wifiAgent.sendLinkProperties(wifiLp)
mCM.reportNetworkConnectivity(wifiNetwork, false)
// The test implicitly checks that no callbacks are sent here, because the next events seen
// by the callbacks are for the new network connecting.
val (newWifiAgent, newWifiNetwork) = connectNetwork(TRANSPORT_WIFI)
testCallback.expectAvailableCallbacks(newWifiNetwork, validated = true)
matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
matchAllCallback.expectCallback<Lost>(wifiNetwork)
wifiAgent.expectCallback<OnNetworkUnwanted>()
}
} }