blob: a4085cde4e210b5b1b6e44b4d435a4276229c127 [file] [log] [blame]
/*
* Copyright (C) 2020 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.connectivity;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkScore.POLICY_EXITING;
import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
import static com.android.net.module.util.CollectionUtils.filter;
import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import com.android.net.module.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
/**
* A class that knows how to find the best network matching a request out of a list of networks.
*/
public class NetworkRanker {
// Historically the legacy ints have been 0~100 in principle (though the highest score in
// AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101.
public static final int LEGACY_INT_MAX = 100;
/**
* A class that can be scored against other scoreables.
*/
public interface Scoreable {
/** Get score of this scoreable */
FullScore getScore();
/** Get capabilities of this scoreable */
NetworkCapabilities getCapsNoCopy();
}
private static final boolean USE_POLICY_RANKING = true;
public NetworkRanker() { }
/**
* Find the best network satisfying this request among the list of passed networks.
*/
@Nullable
public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
@NonNull final Collection<NetworkAgentInfo> nais,
@Nullable final NetworkAgentInfo currentSatisfier) {
final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request));
if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier
if (candidates.size() <= 0) return null; // No network can satisfy this request
if (USE_POLICY_RANKING) {
return getBestNetworkByPolicy(candidates, currentSatisfier);
} else {
return getBestNetworkByLegacyInt(candidates);
}
}
// Transport preference order, if it comes down to that.
private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };
// Function used to partition a list into two working areas depending on whether they
// satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
// items that don't will be put in |negative|.
// This is useful in this file because many of the ranking checks will retain only networks that
// satisfy a predicate if any of them do, but keep them all if all of them do. Having working
// areas is uncustomary in Java, but this function is called in a fairly intensive manner
// and doing allocation quite that often might affect performance quite badly.
private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test,
@NonNull final List<T> positive, @NonNull final List<T> negative) {
positive.clear();
negative.clear();
for (final T item : source) {
if (test.test(item)) {
positive.add(item);
} else {
negative.add(item);
}
}
}
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.
final ArrayList<T> accepted =
new ArrayList<>(candidates.size() /* initialCapacity */);
final ArrayList<T> rejected =
new ArrayList<>(candidates.size() /* initialCapacity */);
// The following tests will search for a network matching a given criterion. They all
// function the same way : if any network matches the criterion, drop from consideration
// all networks that don't. To achieve this, the tests below :
// 1. partition the list of remaining candidates into accepted and rejected networks.
// 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0]
// 3. if multiple remain, keep only the accepted networks and go on to the next criterion.
// Because the working areas will be wiped, a copy of the accepted networks needs to be
// made.
// 4. if none remain, the criterion did not help discriminate so keep them all. As an
// optimization, skip creating a new array and go on to the next criterion.
// If a network is invincible, use it.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE),
accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
// If there is a connected VPN, use it.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN),
accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
// Selected & Accept-unvalidated policy : if any network has both of these, then don't
// choose one that doesn't.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
&& nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
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);
// If any network is not exiting, don't choose one that is.
partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING),
accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
// TODO : If any network is unmetered, don't choose a metered network.
// This can't be implemented immediately because prospective networks are always
// considered unmetered because factories don't know if the network will be metered.
// Saying an unmetered network always beats a metered one would mean that when metered wifi
// is connected, the offer for telephony would beat WiFi but the actual metered network
// would lose, so we'd have an infinite loop where telephony would continually bring up
// a network that is immediately torn down.
// Fix this by getting the agent to tell connectivity whether the network they will
// bring up is metered. Cell knows that in advance, while WiFi has a good estimate and
// can revise it if the network later turns out to be metered.
// partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED),
// 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 for the default subscription, don't choose a network for another
// subscription with the same transport.
partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
accepted, rejected);
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();
rejected.removeIf(
nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
}
accepted.addAll(rejected);
candidates = new ArrayList<>(accepted);
}
if (1 == candidates.size()) return candidates.get(0);
// 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.
for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
accepted, rejected);
if (accepted.size() == 1) return accepted.get(0);
if (accepted.size() > 0 && rejected.size() > 0) {
candidates = new ArrayList<>(accepted);
break;
}
}
// At this point there are still multiple networks passing all the tests above. If any
// of them is the previous satisfier, keep it.
if (candidates.contains(currentSatisfier)) return currentSatisfier;
// If there are still multiple options at this point but none of them is any of the
// transports above, it doesn't matter which is returned. They are all the same.
return candidates.get(0);
}
// TODO : switch to the policy implementation and remove
// Almost equivalent to Collections.max(nais), but allows returning null if no network
// satisfies the request.
private NetworkAgentInfo getBestNetworkByLegacyInt(
@NonNull final Collection<NetworkAgentInfo> nais) {
NetworkAgentInfo bestNetwork = null;
int bestScore = Integer.MIN_VALUE;
for (final NetworkAgentInfo nai : nais) {
final int naiScore = nai.getCurrentScore();
if (naiScore > bestScore) {
bestNetwork = nai;
bestScore = naiScore;
}
}
return bestNetwork;
}
/**
* Returns whether a {@link Scoreable} has a chance to beat a champion network for a request.
*
* Offers are sent by network providers when they think they might be able to make a network
* with the characteristics contained in the offer. If the offer has no chance to beat
* the currently best network for a given request, there is no point in the provider spending
* power trying to find and bring up such a network.
*
* Note that having an offer up does not constitute a commitment from the provider part
* to be able to bring up a network with these characteristics, or a network at all for
* that matter. This is only used to save power by letting providers know when they can't
* beat a current champion.
*
* @param request The request to evaluate against.
* @param champion The currently best network for this request.
* @param contestant The offer.
* @return Whether the offer stands a chance to beat the champion.
*/
public boolean mightBeat(@NonNull final NetworkRequest request,
@Nullable final NetworkAgentInfo champion,
@NonNull final Scoreable contestant) {
// If this network can't even satisfy the request then it can't beat anything, not
// even an absence of network. It can't satisfy it anyway.
if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false;
// If there is no satisfying network, then this network can beat, because some network
// is always better than no network.
if (null == champion) return true;
if (USE_POLICY_RANKING) {
// If there is no champion, the offer can always beat.
// Otherwise rank them.
final ArrayList<Scoreable> candidates = new ArrayList<>();
candidates.add(champion);
candidates.add(contestant);
return contestant == getBestNetworkByPolicy(candidates, champion);
} else {
return mightBeatByLegacyInt(champion.getScore(), contestant);
}
}
/**
* Returns whether a contestant might beat a champion according to the legacy int.
*/
private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore,
@NonNull final Scoreable contestant) {
final int offerIntScore;
if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
// If the offer might have Internet access, then it might validate.
offerIntScore = contestant.getScore().getLegacyIntAsValidated();
} else {
offerIntScore = contestant.getScore().getLegacyInt();
}
return championScore.getLegacyInt() < offerIntScore;
}
}