Make yield-to-bad-wifi behavior backward compatible with R

Yielding cell wins to exiting wifi (whether good or bad).
It loses to bad wifi that's not exiting.

In R, yielding to bad wifi only affects wifis that are
unvalidated, but a wifi that is exiting should still be
dropped in favor of a cell that yields to bad wifi.

I had misunderstood the policy and implemented it wrong.
Now it's implemented right, and has careful tests.

Test: new tests for this
Bug: 186458024
Merged-In: I3c2563d4ae4e3715d0c6270344ba8f7ef067872f
Merged-In: Ib8637100d491e72a2edb837584ce55b7dda58524
Change-Id: Ib8637100d491e72a2edb837584ce55b7dda58524
  (cherry-picked from ag/14486203)
This commit is contained in:
Chalard Jean
2021-05-10 23:00:55 +09:00
committed by Junyu Lai
parent 92c29c56d7
commit 6b2f09272c
2 changed files with 202 additions and 70 deletions

View File

@@ -108,7 +108,58 @@ public class NetworkRanker {
}
}
@Nullable private <T extends Scoreable> T getBestNetworkByPolicy(
private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) {
return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
&& candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
}
/**
* Apply the "yield to bad WiFi" policy.
*
* This function must run immediately after the validation policy.
*
* If any of the accepted networks has the "yield to bad WiFi" policy AND there are some
* bad WiFis in the rejected list, then move the networks with the policy to the rejected
* list. If this leaves no accepted network, then move the bad WiFis back to the accepted list.
*
* This function returns nothing, but will have updated accepted and rejected in-place.
*
* @param accepted networks accepted by the validation policy
* @param rejected networks rejected by the validation policy
*/
private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted,
@NonNull ArrayList<T> rejected) {
if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
// No network with the policy : do nothing.
return;
}
if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) {
// No bad WiFi : do nothing.
return;
}
if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
// All validated networks yield to bad WiFis : keep bad WiFis alongside with the
// yielders. This is important because the yielders need to be compared to the bad
// wifis by the following policies (e.g. exiting).
final ArrayList<T> acceptedYielders = new ArrayList<>(accepted);
final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected);
partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected);
accepted.addAll(acceptedYielders);
return;
}
// Only some of the validated networks yield to bad WiFi : keep only the ones who don't.
final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted);
partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
accepted, rejected);
}
/**
* Get the best network among a list of candidates according to policy.
* @param candidates the candidates
* @param currentSatisfier the current satisfier, or null if none
* @return the best network
*/
@Nullable public <T extends Scoreable> T getBestNetworkByPolicy(
@NonNull List<T> candidates,
@Nullable final T currentSatisfier) {
// Used as working areas.
@@ -148,24 +199,15 @@ public class NetworkRanker {
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
// Yield to bad wifi policy : if any wifi has ever been validated (even if it's now
// unvalidated), and unless it's been explicitly avoided when bad in UI, then keep only
// networks that don't yield to such a wifi network.
final boolean anyWiFiEverValidated = CollectionUtils.any(candidates,
nai -> nai.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
&& nai.getCapsNoCopy().hasTransport(TRANSPORT_WIFI));
if (anyWiFiEverValidated) {
partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
}
// If any network is validated (or should be accepted even if it's not validated), then
// don't choose one that isn't.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED)
|| nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
accepted, rejected);
// Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and
// there are bad WiFis connected, then accept the bad WiFis and reject the networks with
// the policy.
applyYieldToBadWifiPolicy(accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
@@ -194,16 +236,24 @@ public class NetworkRanker {
// subscription with the same transport.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
accepted, rejected);
for (final Scoreable defaultSubNai : accepted) {
// Remove all networks without the DEFAULT_SUBSCRIPTION policy and the same transports
// as a network that has it.
if (accepted.size() > 0) {
// Some networks are primary. For each transport, keep only the primary, but also
// keep all networks for which there isn't a primary (which are now in the |rejected|
// array).
// So for each primary network, remove from |rejected| all networks with the same
// transports as one of the primary networks. The remaining networks should be accepted.
for (final T defaultSubNai : accepted) {
final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
candidates.removeIf(nai -> !nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY)
&& Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
rejected.removeIf(
nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
}
accepted.addAll(rejected);
candidates = new ArrayList<>(accepted);
}
if (1 == candidates.size()) return candidates.get(0);
// It's guaranteed candidates.size() > 0 because there is at least one with the
// TRANSPORT_PRIMARY policy and only those without it were removed.
// If there were no primary network, then candidates.size() > 0 because it didn't
// change from the previous result. If there were, it's guaranteed candidates.size() > 0
// because accepted.size() > 0 above.
// If some of the networks have a better transport than others, keep only the ones with
// the best transports.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright (C) 2021 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.
@@ -17,74 +17,156 @@
package com.android.server.connectivity
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkScore.KEEP_CONNECTED_NONE
import android.net.NetworkScore.POLICY_EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI
import android.os.Build
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD
import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import kotlin.test.assertEquals
import kotlin.test.assertNull
@RunWith(AndroidJUnit4::class)
private fun score(vararg policies: Int) = FullScore(0,
policies.fold(0L) { acc, e -> acc or (1L shl e) }, KEEP_CONNECTED_NONE)
private fun caps(transport: Int) = NetworkCapabilities.Builder().addTransportType(transport).build()
@SmallTest
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class NetworkRankerTest {
private val ranker = NetworkRanker()
private val mRanker = NetworkRanker()
private fun makeNai(satisfy: Boolean, legacyScore: Int) =
mock(NetworkAgentInfo::class.java).also {
doReturn(satisfy).`when`(it).satisfies(any())
val fs = FullScore(legacyScore, 0 /* policies */, KEEP_CONNECTED_NONE)
doReturn(fs).`when`(it).getScore()
val nc = NetworkCapabilities.Builder().build()
doReturn(nc).`when`(it).getCapsNoCopy()
private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
: NetworkRanker.Scoreable {
override fun getScore() = sc
override fun getCapsNoCopy(): NetworkCapabilities = nc
}
@Test
fun testGetBestNetwork() {
val scores = listOf(20, 50, 90, 60, 23, 68)
val nais = scores.map { makeNai(true, it) }
val bestNetwork = nais[2] // The one with the top score
val someRequest = mock(NetworkRequest::class.java)
assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, bestNetwork))
fun testYieldToBadWiFiOneCell() {
// Only cell, it wins
val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
val scores = listOf(winner)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testIgnoreNonSatisfying() {
val nais = listOf(makeNai(true, 20), makeNai(true, 50), makeNai(false, 90),
makeNai(false, 60), makeNai(true, 23), makeNai(false, 68))
val bestNetwork = nais[1] // Top score that's satisfying
val someRequest = mock(NetworkRequest::class.java)
assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, nais[1]))
fun testYieldToBadWiFiOneCellOneBadWiFi() {
// Bad wifi wins against yielding validated cell
val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
caps(TRANSPORT_WIFI))
val scores = listOf(
winner,
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testNoMatch() {
val nais = listOf(makeNai(false, 20), makeNai(false, 50), makeNai(false, 90))
val someRequest = mock(NetworkRequest::class.java)
assertNull(ranker.getBestNetwork(someRequest, nais, null))
fun testYieldToBadWiFiOneCellTwoBadWiFi() {
// Bad wifi wins against yielding validated cell. Prefer the one that's primary.
val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI))
val scores = listOf(
winner,
TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
caps(TRANSPORT_WIFI)),
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testEmpty() {
val someRequest = mock(NetworkRequest::class.java)
assertNull(ranker.getBestNetwork(someRequest, emptyList(), null))
fun testYieldToBadWiFiOneCellTwoBadWiFiOneNotAvoided() {
// Bad wifi ever validated wins against bad wifi that never was validated (or was
// avoided when bad).
val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
caps(TRANSPORT_WIFI))
val scores = listOf(
winner,
TestScore(score(), caps(TRANSPORT_WIFI)),
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
// Make sure the ranker is "stable" (as in stable sort), that is, it always returns the FIRST
// network satisfying the request if multiple of them have the same score.
@Test
fun testStable() {
val nais1 = listOf(makeNai(true, 30), makeNai(true, 30), makeNai(true, 30),
makeNai(true, 30), makeNai(true, 30), makeNai(true, 30))
val someRequest = mock(NetworkRequest::class.java)
assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1, nais1[0]))
fun testYieldToBadWiFiOneCellOneBadWiFiOneGoodWiFi() {
// Good wifi wins
val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
val scores = listOf(
winner,
TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
val nais2 = listOf(makeNai(true, 30), makeNai(true, 50), makeNai(true, 20),
makeNai(true, 50), makeNai(true, 50), makeNai(true, 40))
assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2, nais2[1]))
@Test
fun testYieldToBadWiFiTwoCellsOneBadWiFi() {
// Cell that doesn't yield wins over cell that yields and bad wifi
val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR))
val scores = listOf(
winner,
TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testYieldToBadWiFiTwoCellsOneBadWiFiOneGoodWiFi() {
// Good wifi wins over cell that doesn't yield and cell that yields
val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
val scores = listOf(
winner,
TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)),
TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testYieldToBadWiFiOneExitingGoodWiFi() {
// Yielding cell wins over good exiting wifi
val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
val scores = listOf(
winner,
TestScore(score(POLICY_IS_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
@Test
fun testYieldToBadWiFiOneExitingBadWiFi() {
// Yielding cell wins over bad exiting wifi
val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
caps(TRANSPORT_CELLULAR))
val scores = listOf(
winner,
TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
POLICY_EXITING), caps(TRANSPORT_WIFI))
)
assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
}
}