1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity; 18 19 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; 20 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; 21 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; 22 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 23 import static android.net.NetworkScore.POLICY_EXITING; 24 import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY; 25 import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI; 26 27 import static com.android.net.module.util.CollectionUtils.filter; 28 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED; 29 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED; 30 import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD; 31 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE; 32 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED; 33 import static com.android.server.connectivity.FullScore.POLICY_IS_VPN; 34 35 import android.annotation.NonNull; 36 import android.annotation.Nullable; 37 import android.net.NetworkCapabilities; 38 import android.net.NetworkRequest; 39 40 import com.android.net.module.util.CollectionUtils; 41 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Collection; 45 import java.util.List; 46 import java.util.function.Predicate; 47 48 /** 49 * A class that knows how to find the best network matching a request out of a list of networks. 50 */ 51 public class NetworkRanker { 52 // Historically the legacy ints have been 0~100 in principle (though the highest score in 53 // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101. 54 public static final int LEGACY_INT_MAX = 100; 55 56 /** 57 * A class that can be scored against other scoreables. 58 */ 59 public interface Scoreable { 60 /** Get score of this scoreable */ getScore()61 FullScore getScore(); 62 /** Get capabilities of this scoreable */ getCapsNoCopy()63 NetworkCapabilities getCapsNoCopy(); 64 } 65 66 private static final boolean USE_POLICY_RANKING = true; 67 NetworkRanker()68 public NetworkRanker() { } 69 70 /** 71 * Find the best network satisfying this request among the list of passed networks. 72 */ 73 @Nullable getBestNetwork(@onNull final NetworkRequest request, @NonNull final Collection<NetworkAgentInfo> nais, @Nullable final NetworkAgentInfo currentSatisfier)74 public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request, 75 @NonNull final Collection<NetworkAgentInfo> nais, 76 @Nullable final NetworkAgentInfo currentSatisfier) { 77 final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request)); 78 if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier 79 if (candidates.size() <= 0) return null; // No network can satisfy this request 80 if (USE_POLICY_RANKING) { 81 return getBestNetworkByPolicy(candidates, currentSatisfier); 82 } else { 83 return getBestNetworkByLegacyInt(candidates); 84 } 85 } 86 87 // Transport preference order, if it comes down to that. 88 private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI, 89 TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR }; 90 91 // Function used to partition a list into two working areas depending on whether they 92 // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all 93 // items that don't will be put in |negative|. 94 // This is useful in this file because many of the ranking checks will retain only networks that 95 // satisfy a predicate if any of them do, but keep them all if all of them do. Having working 96 // areas is uncustomary in Java, but this function is called in a fairly intensive manner 97 // and doing allocation quite that often might affect performance quite badly. partitionInto(@onNull final List<T> source, @NonNull Predicate<T> test, @NonNull final List<T> positive, @NonNull final List<T> negative)98 private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test, 99 @NonNull final List<T> positive, @NonNull final List<T> negative) { 100 positive.clear(); 101 negative.clear(); 102 for (final T item : source) { 103 if (test.test(item)) { 104 positive.add(item); 105 } else { 106 negative.add(item); 107 } 108 } 109 } 110 isBadWiFi(@onNull final T candidate)111 private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) { 112 return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD) 113 && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI); 114 } 115 116 /** 117 * Apply the "yield to bad WiFi" policy. 118 * 119 * This function must run immediately after the validation policy. 120 * 121 * If any of the accepted networks has the "yield to bad WiFi" policy AND there are some 122 * bad WiFis in the rejected list, then move the networks with the policy to the rejected 123 * list. If this leaves no accepted network, then move the bad WiFis back to the accepted list. 124 * 125 * This function returns nothing, but will have updated accepted and rejected in-place. 126 * 127 * @param accepted networks accepted by the validation policy 128 * @param rejected networks rejected by the validation policy 129 */ applyYieldToBadWifiPolicy(@onNull ArrayList<T> accepted, @NonNull ArrayList<T> rejected)130 private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted, 131 @NonNull ArrayList<T> rejected) { 132 if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { 133 // No network with the policy : do nothing. 134 return; 135 } 136 if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) { 137 // No bad WiFi : do nothing. 138 return; 139 } 140 if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { 141 // All validated networks yield to bad WiFis : keep bad WiFis alongside with the 142 // yielders. This is important because the yielders need to be compared to the bad 143 // wifis by the following policies (e.g. exiting). 144 final ArrayList<T> acceptedYielders = new ArrayList<>(accepted); 145 final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected); 146 partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected); 147 accepted.addAll(acceptedYielders); 148 return; 149 } 150 // Only some of the validated networks yield to bad WiFi : keep only the ones who don't. 151 final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted); 152 partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI), 153 accepted, rejected); 154 } 155 156 /** 157 * Get the best network among a list of candidates according to policy. 158 * @param candidates the candidates 159 * @param currentSatisfier the current satisfier, or null if none 160 * @return the best network 161 */ getBestNetworkByPolicy( @onNull List<T> candidates, @Nullable final T currentSatisfier)162 @Nullable public <T extends Scoreable> T getBestNetworkByPolicy( 163 @NonNull List<T> candidates, 164 @Nullable final T currentSatisfier) { 165 // Used as working areas. 166 final ArrayList<T> accepted = 167 new ArrayList<>(candidates.size() /* initialCapacity */); 168 final ArrayList<T> rejected = 169 new ArrayList<>(candidates.size() /* initialCapacity */); 170 171 // The following tests will search for a network matching a given criterion. They all 172 // function the same way : if any network matches the criterion, drop from consideration 173 // all networks that don't. To achieve this, the tests below : 174 // 1. partition the list of remaining candidates into accepted and rejected networks. 175 // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0] 176 // 3. if multiple remain, keep only the accepted networks and go on to the next criterion. 177 // Because the working areas will be wiped, a copy of the accepted networks needs to be 178 // made. 179 // 4. if none remain, the criterion did not help discriminate so keep them all. As an 180 // optimization, skip creating a new array and go on to the next criterion. 181 182 // If a network is invincible, use it. 183 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE), 184 accepted, rejected); 185 if (accepted.size() == 1) return accepted.get(0); 186 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 187 188 // If there is a connected VPN, use it. 189 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN), 190 accepted, rejected); 191 if (accepted.size() == 1) return accepted.get(0); 192 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 193 194 // Selected & Accept-unvalidated policy : if any network has both of these, then don't 195 // choose one that doesn't. 196 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED) 197 && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), 198 accepted, rejected); 199 if (accepted.size() == 1) return accepted.get(0); 200 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 201 202 // If any network is validated (or should be accepted even if it's not validated), then 203 // don't choose one that isn't. 204 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED) 205 || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), 206 accepted, rejected); 207 // Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and 208 // there are bad WiFis connected, then accept the bad WiFis and reject the networks with 209 // the policy. 210 applyYieldToBadWifiPolicy(accepted, rejected); 211 if (accepted.size() == 1) return accepted.get(0); 212 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 213 214 // If any network is not exiting, don't choose one that is. 215 partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING), 216 accepted, rejected); 217 if (accepted.size() == 1) return accepted.get(0); 218 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 219 220 // TODO : If any network is unmetered, don't choose a metered network. 221 // This can't be implemented immediately because prospective networks are always 222 // considered unmetered because factories don't know if the network will be metered. 223 // Saying an unmetered network always beats a metered one would mean that when metered wifi 224 // is connected, the offer for telephony would beat WiFi but the actual metered network 225 // would lose, so we'd have an infinite loop where telephony would continually bring up 226 // a network that is immediately torn down. 227 // Fix this by getting the agent to tell connectivity whether the network they will 228 // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and 229 // can revise it if the network later turns out to be metered. 230 // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED), 231 // accepted, rejected); 232 // if (accepted.size() == 1) return accepted.get(0); 233 // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 234 235 // If any network is for the default subscription, don't choose a network for another 236 // subscription with the same transport. 237 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY), 238 accepted, rejected); 239 if (accepted.size() > 0) { 240 // Some networks are primary for their transport. For each transport, keep only the 241 // primary, but also keep all networks for which there isn't a primary (which are now 242 // in the |rejected| array). 243 // So for each primary network, remove from |rejected| all networks with the same 244 // transports as one of the primary networks. The remaining networks should be accepted. 245 for (final T defaultSubNai : accepted) { 246 final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes(); 247 rejected.removeIf( 248 nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes())); 249 } 250 // Now the |rejected| list contains networks with transports for which there isn't 251 // a primary network. Add them back to the candidates. 252 accepted.addAll(rejected); 253 candidates = new ArrayList<>(accepted); 254 } 255 if (1 == candidates.size()) return candidates.get(0); 256 // If there were no primary network, then candidates.size() > 0 because it didn't 257 // change from the previous result. If there were, it's guaranteed candidates.size() > 0 258 // because accepted.size() > 0 above. 259 260 // If some of the networks have a better transport than others, keep only the ones with 261 // the best transports. 262 for (final int transport : PREFERRED_TRANSPORTS_ORDER) { 263 partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport), 264 accepted, rejected); 265 if (accepted.size() == 1) return accepted.get(0); 266 if (accepted.size() > 0 && rejected.size() > 0) { 267 candidates = new ArrayList<>(accepted); 268 break; 269 } 270 } 271 272 // At this point there are still multiple networks passing all the tests above. If any 273 // of them is the previous satisfier, keep it. 274 if (candidates.contains(currentSatisfier)) return currentSatisfier; 275 276 // If there are still multiple options at this point but none of them is any of the 277 // transports above, it doesn't matter which is returned. They are all the same. 278 return candidates.get(0); 279 } 280 281 // TODO : switch to the policy implementation and remove 282 // Almost equivalent to Collections.max(nais), but allows returning null if no network 283 // satisfies the request. getBestNetworkByLegacyInt( @onNull final Collection<NetworkAgentInfo> nais)284 private NetworkAgentInfo getBestNetworkByLegacyInt( 285 @NonNull final Collection<NetworkAgentInfo> nais) { 286 NetworkAgentInfo bestNetwork = null; 287 int bestScore = Integer.MIN_VALUE; 288 for (final NetworkAgentInfo nai : nais) { 289 final int naiScore = nai.getCurrentScore(); 290 if (naiScore > bestScore) { 291 bestNetwork = nai; 292 bestScore = naiScore; 293 } 294 } 295 return bestNetwork; 296 } 297 298 /** 299 * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request. 300 * 301 * Offers are sent by network providers when they think they might be able to make a network 302 * with the characteristics contained in the offer. If the offer has no chance to beat 303 * the currently best network for a given request, there is no point in the provider spending 304 * power trying to find and bring up such a network. 305 * 306 * Note that having an offer up does not constitute a commitment from the provider part 307 * to be able to bring up a network with these characteristics, or a network at all for 308 * that matter. This is only used to save power by letting providers know when they can't 309 * beat a current champion. 310 * 311 * @param request The request to evaluate against. 312 * @param champion The currently best network for this request. 313 * @param contestant The offer. 314 * @return Whether the offer stands a chance to beat the champion. 315 */ mightBeat(@onNull final NetworkRequest request, @Nullable final NetworkAgentInfo champion, @NonNull final Scoreable contestant)316 public boolean mightBeat(@NonNull final NetworkRequest request, 317 @Nullable final NetworkAgentInfo champion, 318 @NonNull final Scoreable contestant) { 319 // If this network can't even satisfy the request then it can't beat anything, not 320 // even an absence of network. It can't satisfy it anyway. 321 if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false; 322 // If there is no satisfying network, then this network can beat, because some network 323 // is always better than no network. 324 if (null == champion) return true; 325 if (USE_POLICY_RANKING) { 326 // If there is no champion, the offer can always beat. 327 // Otherwise rank them. 328 final ArrayList<Scoreable> candidates = new ArrayList<>(); 329 candidates.add(champion); 330 candidates.add(contestant); 331 return contestant == getBestNetworkByPolicy(candidates, champion); 332 } else { 333 return mightBeatByLegacyInt(champion.getScore(), contestant); 334 } 335 } 336 337 /** 338 * Returns whether a contestant might beat a champion according to the legacy int. 339 */ mightBeatByLegacyInt(@ullable final FullScore championScore, @NonNull final Scoreable contestant)340 private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore, 341 @NonNull final Scoreable contestant) { 342 final int offerIntScore; 343 if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { 344 // If the offer might have Internet access, then it might validate. 345 offerIntScore = contestant.getScore().getLegacyIntAsValidated(); 346 } else { 347 offerIntScore = contestant.getScore().getLegacyInt(); 348 } 349 return championScore.getLegacyInt() < offerIntScore; 350 } 351 } 352