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.NET_CAPABILITY_CAPTIVE_PORTAL; 20 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; 21 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; 22 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; 23 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 24 import static android.net.NetworkScore.POLICY_EXITING; 25 import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY; 26 import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI; 27 28 import static com.android.net.module.util.CollectionUtils.filter; 29 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED; 30 import static com.android.server.connectivity.FullScore.POLICY_AVOIDED_WHEN_UNVALIDATED; 31 import static com.android.server.connectivity.FullScore.POLICY_EVER_EVALUATED; 32 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED; 33 import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED; 34 import static com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED; 35 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE; 36 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED; 37 import static com.android.server.connectivity.FullScore.POLICY_IS_VPN; 38 39 import android.annotation.NonNull; 40 import android.annotation.Nullable; 41 import android.net.NetworkCapabilities; 42 import android.net.NetworkRequest; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.net.module.util.CollectionUtils; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.function.Predicate; 53 54 /** 55 * A class that knows how to find the best network matching a request out of a list of networks. 56 */ 57 public class NetworkRanker { 58 /** 59 * Home for all configurations of NetworkRanker 60 */ 61 public static final class Configuration { 62 private final boolean mActivelyPreferBadWifi; 63 Configuration(final boolean activelyPreferBadWifi)64 public Configuration(final boolean activelyPreferBadWifi) { 65 this.mActivelyPreferBadWifi = activelyPreferBadWifi; 66 } 67 68 /** 69 * @see MultinetworkPolicyTracker#getActivelyPreferBadWifi() 70 */ activelyPreferBadWifi()71 public boolean activelyPreferBadWifi() { 72 return mActivelyPreferBadWifi; 73 } 74 } 75 @NonNull private volatile Configuration mConf; 76 77 // Historically the legacy ints have been 0~100 in principle (though the highest score in 78 // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101. 79 public static final int LEGACY_INT_MAX = 100; 80 81 /** 82 * A class that can be scored against other scoreables. 83 */ 84 public interface Scoreable { 85 /** Get score of this scoreable */ getScore()86 FullScore getScore(); 87 /** Get capabilities of this scoreable */ getCapsNoCopy()88 NetworkCapabilities getCapsNoCopy(); 89 } 90 NetworkRanker(@onNull final Configuration conf)91 public NetworkRanker(@NonNull final Configuration conf) { 92 // Because mConf is volatile, the only way it could be seen null would be an access to it 93 // on some other thread during this constructor. But this is not possible because mConf is 94 // private and `this` doesn't escape this constructor. 95 setConfiguration(conf); 96 } 97 setConfiguration(@onNull final Configuration conf)98 public void setConfiguration(@NonNull final Configuration conf) { 99 mConf = Objects.requireNonNull(conf); 100 } 101 102 // There shouldn't be a use case outside of testing 103 @VisibleForTesting getConfiguration()104 public Configuration getConfiguration() { 105 return mConf; 106 } 107 108 /** 109 * Find the best network satisfying this request among the list of passed networks. 110 */ 111 @Nullable getBestNetwork(@onNull final NetworkRequest request, @NonNull final Collection<NetworkAgentInfo> nais, @Nullable final NetworkAgentInfo currentSatisfier)112 public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request, 113 @NonNull final Collection<NetworkAgentInfo> nais, 114 @Nullable final NetworkAgentInfo currentSatisfier) { 115 final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request)); 116 if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier 117 if (candidates.size() <= 0) return null; // No network can satisfy this request 118 return getBestNetworkByPolicy(candidates, currentSatisfier); 119 } 120 121 // Transport preference order, if it comes down to that. 122 private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI, 123 TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR }; 124 125 // Function used to partition a list into two working areas depending on whether they 126 // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all 127 // items that don't will be put in |negative|. 128 // This is useful in this file because many of the ranking checks will retain only networks that 129 // satisfy a predicate if any of them do, but keep them all if all of them do. Having working 130 // areas is uncustomary in Java, but this function is called in a fairly intensive manner 131 // 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)132 private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test, 133 @NonNull final List<T> positive, @NonNull final List<T> negative) { 134 positive.clear(); 135 negative.clear(); 136 for (final T item : source) { 137 if (test.test(item)) { 138 positive.add(item); 139 } else { 140 negative.add(item); 141 } 142 } 143 } 144 145 /** 146 * Returns whether the wifi passed as an argument is a preferred network to yielding cell. 147 * 148 * When comparing bad wifi to cell with POLICY_YIELD_TO_BAD_WIFI, it may be necessary to 149 * know if a particular bad wifi is preferred to such a cell network. This method computes 150 * and returns this. 151 * 152 * @param candidate a bad wifi to evaluate 153 * @return whether this candidate is preferred to cell with POLICY_YIELD_TO_BAD_WIFI 154 */ isPreferredBadWiFi(@onNull final T candidate)155 private <T extends Scoreable> boolean isPreferredBadWiFi(@NonNull final T candidate) { 156 final FullScore score = candidate.getScore(); 157 final NetworkCapabilities caps = candidate.getCapsNoCopy(); 158 159 // Whatever the policy, only WiFis can be preferred bad WiFis. 160 if (!caps.hasTransport(TRANSPORT_WIFI)) return false; 161 // Validated networks aren't bad networks, so a fortiori can't be preferred bad WiFis. 162 if (score.hasPolicy(POLICY_IS_VALIDATED)) return false; 163 // A WiFi that the user explicitly wanted to avoid in UI is never a preferred bad WiFi. 164 if (score.hasPolicy(POLICY_AVOIDED_WHEN_UNVALIDATED)) return false; 165 166 if (mConf.activelyPreferBadWifi()) { 167 // If a network is still evaluating, don't prefer it. 168 if (!score.hasPolicy(POLICY_EVER_EVALUATED)) return false; 169 170 // If a network is not a captive portal, then prefer it. 171 if (!caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return true; 172 173 // If it's a captive portal, prefer it if it previously validated but is no longer 174 // validated (i.e., the user logged in in the past, but later the portal closed). 175 return score.hasPolicy(POLICY_EVER_VALIDATED); 176 } else { 177 // Under the original "prefer bad WiFi" policy, only networks that have ever validated 178 // are preferred. 179 return score.hasPolicy(POLICY_EVER_VALIDATED); 180 } 181 } 182 183 /** 184 * Apply the "yield to bad WiFi" policy. 185 * 186 * This function must run immediately after the validation policy. 187 * 188 * If any of the accepted networks has the "yield to bad WiFi" policy AND there are some 189 * bad WiFis in the rejected list, then move the networks with the policy to the rejected 190 * list. If this leaves no accepted network, then move the bad WiFis back to the accepted list. 191 * 192 * This function returns nothing, but will have updated accepted and rejected in-place. 193 * 194 * @param accepted networks accepted by the validation policy 195 * @param rejected networks rejected by the validation policy 196 */ applyYieldToBadWifiPolicy(@onNull ArrayList<T> accepted, @NonNull ArrayList<T> rejected)197 private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted, 198 @NonNull ArrayList<T> rejected) { 199 if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { 200 // No network with the policy : do nothing. 201 return; 202 } 203 if (!CollectionUtils.any(rejected, n -> isPreferredBadWiFi(n))) { 204 // No bad WiFi : do nothing. 205 return; 206 } 207 if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { 208 // All validated networks yield to bad WiFis : keep bad WiFis alongside with the 209 // yielders. This is important because the yielders need to be compared to the bad 210 // wifis by the following policies (e.g. exiting). 211 final ArrayList<T> acceptedYielders = new ArrayList<>(accepted); 212 final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected); 213 partitionInto(rejectedWithBadWiFis, n -> isPreferredBadWiFi(n), accepted, rejected); 214 accepted.addAll(acceptedYielders); 215 return; 216 } 217 // Only some of the validated networks yield to bad WiFi : keep only the ones who don't. 218 final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted); 219 partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI), 220 accepted, rejected); 221 } 222 223 /** 224 * Get the best network among a list of candidates according to policy. 225 * @param candidates the candidates 226 * @param currentSatisfier the current satisfier, or null if none 227 * @return the best network 228 */ getBestNetworkByPolicy( @onNull List<T> candidates, @Nullable final T currentSatisfier)229 @Nullable public <T extends Scoreable> T getBestNetworkByPolicy( 230 @NonNull List<T> candidates, 231 @Nullable final T currentSatisfier) { 232 // Used as working areas. 233 final ArrayList<T> accepted = 234 new ArrayList<>(candidates.size() /* initialCapacity */); 235 final ArrayList<T> rejected = 236 new ArrayList<>(candidates.size() /* initialCapacity */); 237 238 // The following tests will search for a network matching a given criterion. They all 239 // function the same way : if any network matches the criterion, drop from consideration 240 // all networks that don't. To achieve this, the tests below : 241 // 1. partition the list of remaining candidates into accepted and rejected networks. 242 // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0] 243 // 3. if multiple remain, keep only the accepted networks and go on to the next criterion. 244 // Because the working areas will be wiped, a copy of the accepted networks needs to be 245 // made. 246 // 4. if none remain, the criterion did not help discriminate so keep them all. As an 247 // optimization, skip creating a new array and go on to the next criterion. 248 249 // If a network is invincible, use it. 250 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE), 251 accepted, rejected); 252 if (accepted.size() == 1) return accepted.get(0); 253 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 254 255 // If there is a connected VPN, use it. 256 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN), 257 accepted, rejected); 258 if (accepted.size() == 1) return accepted.get(0); 259 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 260 261 // Selected & Accept-unvalidated policy : if any network has both of these, then don't 262 // choose one that doesn't. 263 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED) 264 && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), 265 accepted, rejected); 266 if (accepted.size() == 1) return accepted.get(0); 267 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 268 269 // If any network is validated (or should be accepted even if it's not validated), then 270 // don't choose one that isn't. 271 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED) 272 || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), 273 accepted, rejected); 274 // Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and 275 // there are bad WiFis connected, then accept the bad WiFis and reject the networks with 276 // the policy. 277 applyYieldToBadWifiPolicy(accepted, rejected); 278 if (accepted.size() == 1) return accepted.get(0); 279 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 280 281 // If any network is not exiting, don't choose one that is. 282 partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING), 283 accepted, rejected); 284 if (accepted.size() == 1) return accepted.get(0); 285 if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 286 287 // TODO : If any network is unmetered, don't choose a metered network. 288 // This can't be implemented immediately because prospective networks are always 289 // considered unmetered because factories don't know if the network will be metered. 290 // Saying an unmetered network always beats a metered one would mean that when metered wifi 291 // is connected, the offer for telephony would beat WiFi but the actual metered network 292 // would lose, so we'd have an infinite loop where telephony would continually bring up 293 // a network that is immediately torn down. 294 // Fix this by getting the agent to tell connectivity whether the network they will 295 // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and 296 // can revise it if the network later turns out to be metered. 297 // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED), 298 // accepted, rejected); 299 // if (accepted.size() == 1) return accepted.get(0); 300 // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); 301 302 // If any network is for the default subscription, don't choose a network for another 303 // subscription with the same transport. 304 partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY), 305 accepted, rejected); 306 if (accepted.size() > 0) { 307 // Some networks are primary for their transport. For each transport, keep only the 308 // primary, but also keep all networks for which there isn't a primary (which are now 309 // in the |rejected| array). 310 // So for each primary network, remove from |rejected| all networks with the same 311 // transports as one of the primary networks. The remaining networks should be accepted. 312 for (final T defaultSubNai : accepted) { 313 final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes(); 314 rejected.removeIf( 315 nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes())); 316 } 317 // Now the |rejected| list contains networks with transports for which there isn't 318 // a primary network. Add them back to the candidates. 319 accepted.addAll(rejected); 320 candidates = new ArrayList<>(accepted); 321 } 322 if (1 == candidates.size()) return candidates.get(0); 323 // If there were no primary network, then candidates.size() > 0 because it didn't 324 // change from the previous result. If there were, it's guaranteed candidates.size() > 0 325 // because accepted.size() > 0 above. 326 327 // If some of the networks have a better transport than others, keep only the ones with 328 // the best transports. 329 for (final int transport : PREFERRED_TRANSPORTS_ORDER) { 330 partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport), 331 accepted, rejected); 332 if (accepted.size() == 1) return accepted.get(0); 333 if (accepted.size() > 0 && rejected.size() > 0) { 334 candidates = new ArrayList<>(accepted); 335 break; 336 } 337 } 338 339 // If two networks are equivalent, and one has been destroyed pending replacement, keep the 340 // other one. This ensures that when the replacement connects, it's preferred. 341 partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_IS_DESTROYED), 342 accepted, rejected); 343 if (accepted.size() == 1) return accepted.get(0); 344 if (accepted.size() > 0 && rejected.size() > 0) { 345 candidates = new ArrayList<>(accepted); 346 } 347 348 // At this point there are still multiple networks passing all the tests above. If any 349 // of them is the previous satisfier, keep it. 350 if (candidates.contains(currentSatisfier)) return currentSatisfier; 351 352 // If there are still multiple options at this point but none of them is any of the 353 // transports above, it doesn't matter which is returned. They are all the same. 354 return candidates.get(0); 355 } 356 357 /** 358 * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request. 359 * 360 * Offers are sent by network providers when they think they might be able to make a network 361 * with the characteristics contained in the offer. If the offer has no chance to beat 362 * the currently best network for a given request, there is no point in the provider spending 363 * power trying to find and bring up such a network. 364 * 365 * Note that having an offer up does not constitute a commitment from the provider part 366 * to be able to bring up a network with these characteristics, or a network at all for 367 * that matter. This is only used to save power by letting providers know when they can't 368 * beat a current champion. 369 * 370 * @param request The request to evaluate against. 371 * @param champion The currently best network for this request. 372 * @param contestant The offer. 373 * @return Whether the offer stands a chance to beat the champion. 374 */ mightBeat(@onNull final NetworkRequest request, @Nullable final NetworkAgentInfo champion, @NonNull final Scoreable contestant)375 public boolean mightBeat(@NonNull final NetworkRequest request, 376 @Nullable final NetworkAgentInfo champion, 377 @NonNull final Scoreable contestant) { 378 // If this network can't even satisfy the request then it can't beat anything, not 379 // even an absence of network. It can't satisfy it anyway. 380 if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false; 381 // If there is no satisfying network, then this network can beat, because some network 382 // is always better than no network. 383 if (null == champion) return true; 384 // If there is no champion, the offer can always beat. 385 // Otherwise rank them. 386 final ArrayList<Scoreable> candidates = new ArrayList<>(); 387 candidates.add(champion); 388 candidates.add(contestant); 389 return contestant == getBestNetworkByPolicy(candidates, champion); 390 } 391 } 392