1 /* 2 * Copyright 2018 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.wifi; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.net.MacAddress; 23 import android.net.wifi.ScanResult; 24 import android.net.wifi.WifiConfiguration; 25 import android.util.ArrayMap; 26 27 import com.android.internal.util.Preconditions; 28 import com.android.server.wifi.proto.WifiScoreCardProto; 29 import com.android.wifi.resources.R; 30 31 import java.util.ArrayList; 32 import java.util.Collection; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 import java.util.StringJoiner; 38 import java.util.stream.Collectors; 39 40 /** 41 * Candidates for network selection 42 */ 43 public class WifiCandidates { 44 private static final String TAG = "WifiCandidates"; 45 WifiCandidates(@onNull WifiScoreCard wifiScoreCard, @NonNull Context context)46 public WifiCandidates(@NonNull WifiScoreCard wifiScoreCard, @NonNull Context context) { 47 this(wifiScoreCard, context, Collections.EMPTY_LIST); 48 } 49 WifiCandidates(@onNull WifiScoreCard wifiScoreCard, @NonNull Context context, @NonNull List<Candidate> candidates)50 public WifiCandidates(@NonNull WifiScoreCard wifiScoreCard, @NonNull Context context, 51 @NonNull List<Candidate> candidates) { 52 mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard); 53 mContext = context; 54 for (Candidate c : candidates) { 55 mCandidates.put(c.getKey(), c); 56 } 57 } 58 59 private final WifiScoreCard mWifiScoreCard; 60 private final Context mContext; 61 62 /** 63 * Represents a connectable candidate. 64 */ 65 public interface Candidate { 66 /** 67 * Gets the Key, which contains the SSID, BSSID, security type, and config id. 68 * 69 * Generally, a CandidateScorer should not need to use this. 70 */ getKey()71 @Nullable Key getKey(); 72 73 /** 74 * Gets the config id. 75 */ getNetworkConfigId()76 int getNetworkConfigId(); 77 /** 78 * Returns true for an open network. 79 */ isOpenNetwork()80 boolean isOpenNetwork(); 81 /** 82 * Returns true for a passpoint network. 83 */ isPasspoint()84 boolean isPasspoint(); 85 /** 86 * Returns true for an ephemeral network. 87 */ isEphemeral()88 boolean isEphemeral(); 89 /** 90 * Returns true for a trusted network. 91 */ isTrusted()92 boolean isTrusted(); 93 /** 94 * Returns true if suggestion came from a carrier or privileged app. 95 */ isCarrierOrPrivileged()96 boolean isCarrierOrPrivileged(); 97 /** 98 * Returns true for a metered network. 99 */ isMetered()100 boolean isMetered(); 101 102 /** 103 * Returns true if network doesn't have internet access during last connection 104 */ hasNoInternetAccess()105 boolean hasNoInternetAccess(); 106 107 /** 108 * Returns true if network is expected not to have Internet access 109 * (e.g., a wireless printer, a Chromecast hotspot, etc.). 110 */ isNoInternetAccessExpected()111 boolean isNoInternetAccessExpected(); 112 113 /** 114 * Returns the ID of the nominator that provided the candidate. 115 */ 116 @WifiNetworkSelector.NetworkNominator.NominatorId getNominatorId()117 int getNominatorId(); 118 119 /** 120 * Returns true if the candidate is in the same network as the 121 * current connection. 122 */ isCurrentNetwork()123 boolean isCurrentNetwork(); 124 /** 125 * Return true if the candidate is currently connected. 126 */ isCurrentBssid()127 boolean isCurrentBssid(); 128 /** 129 * Returns a value between 0 and 1. 130 * 131 * 1.0 means the network was recently selected by the user or an app. 132 * 0.0 means not recently selected by user or app. 133 */ getLastSelectionWeight()134 double getLastSelectionWeight(); 135 /** 136 * Gets the scan RSSI. 137 */ getScanRssi()138 int getScanRssi(); 139 /** 140 * Gets the scan frequency. 141 */ getFrequency()142 int getFrequency(); 143 /** 144 * Gets the predicted throughput in Mbps. 145 */ getPredictedThroughputMbps()146 int getPredictedThroughputMbps(); 147 /** 148 * Estimated probability of getting internet access (percent 0-100). 149 */ getEstimatedPercentInternetAvailability()150 int getEstimatedPercentInternetAvailability(); 151 /** 152 * Gets statistics from the scorecard. 153 */ getEventStatistics(WifiScoreCardProto.Event event)154 @Nullable WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event); 155 } 156 157 /** 158 * Represents a connectable candidate 159 */ 160 private static class CandidateImpl implements Candidate { 161 private final Key mKey; // SSID/sectype/BSSID/configId 162 private final @WifiNetworkSelector.NetworkNominator.NominatorId int mNominatorId; 163 private final int mScanRssi; 164 private final int mFrequency; 165 private final double mLastSelectionWeight; 166 private final WifiScoreCard.PerBssid mPerBssid; // For accessing the scorecard entry 167 private final boolean mIsCurrentNetwork; 168 private final boolean mIsCurrentBssid; 169 private final boolean mIsMetered; 170 private final boolean mHasNoInternetAccess; 171 private final boolean mIsNoInternetAccessExpected; 172 private final boolean mIsOpenNetwork; 173 private final boolean mPasspoint; 174 private final boolean mEphemeral; 175 private final boolean mTrusted; 176 private final boolean mCarrierOrPrivileged; 177 private final int mPredictedThroughputMbps; 178 private final int mEstimatedPercentInternetAvailability; 179 CandidateImpl(Key key, WifiConfiguration config, WifiScoreCard.PerBssid perBssid, @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, int scanRssi, int frequency, double lastSelectionWeight, boolean isCurrentNetwork, boolean isCurrentBssid, boolean isMetered, boolean isCarrierOrPrivileged, int predictedThroughputMbps)180 CandidateImpl(Key key, WifiConfiguration config, 181 WifiScoreCard.PerBssid perBssid, 182 @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, 183 int scanRssi, 184 int frequency, 185 double lastSelectionWeight, 186 boolean isCurrentNetwork, 187 boolean isCurrentBssid, 188 boolean isMetered, 189 boolean isCarrierOrPrivileged, 190 int predictedThroughputMbps) { 191 this.mKey = key; 192 this.mNominatorId = nominatorId; 193 this.mScanRssi = scanRssi; 194 this.mFrequency = frequency; 195 this.mPerBssid = perBssid; 196 this.mLastSelectionWeight = lastSelectionWeight; 197 this.mIsCurrentNetwork = isCurrentNetwork; 198 this.mIsCurrentBssid = isCurrentBssid; 199 this.mIsMetered = isMetered; 200 this.mHasNoInternetAccess = config.hasNoInternetAccess(); 201 this.mIsNoInternetAccessExpected = config.isNoInternetAccessExpected(); 202 this.mIsOpenNetwork = WifiConfigurationUtil.isConfigForOpenNetwork(config); 203 this.mPasspoint = config.isPasspoint(); 204 this.mEphemeral = config.isEphemeral(); 205 this.mTrusted = config.trusted; 206 this.mCarrierOrPrivileged = isCarrierOrPrivileged; 207 this.mPredictedThroughputMbps = predictedThroughputMbps; 208 this.mEstimatedPercentInternetAvailability = perBssid == null ? 50 : 209 perBssid.estimatePercentInternetAvailability(); 210 } 211 212 @Override getKey()213 public Key getKey() { 214 return mKey; 215 } 216 217 @Override getNetworkConfigId()218 public int getNetworkConfigId() { 219 return mKey.networkId; 220 } 221 222 @Override isOpenNetwork()223 public boolean isOpenNetwork() { 224 return mIsOpenNetwork; 225 } 226 227 @Override isPasspoint()228 public boolean isPasspoint() { 229 return mPasspoint; 230 } 231 232 @Override isEphemeral()233 public boolean isEphemeral() { 234 return mEphemeral; 235 } 236 237 @Override isTrusted()238 public boolean isTrusted() { 239 return mTrusted; 240 } 241 242 @Override isCarrierOrPrivileged()243 public boolean isCarrierOrPrivileged() { 244 return mCarrierOrPrivileged; 245 } 246 247 @Override isMetered()248 public boolean isMetered() { 249 return mIsMetered; 250 } 251 252 @Override hasNoInternetAccess()253 public boolean hasNoInternetAccess() { 254 return mHasNoInternetAccess; 255 } 256 257 @Override isNoInternetAccessExpected()258 public boolean isNoInternetAccessExpected() { 259 return mIsNoInternetAccessExpected; 260 } 261 262 @Override getNominatorId()263 public @WifiNetworkSelector.NetworkNominator.NominatorId int getNominatorId() { 264 return mNominatorId; 265 } 266 267 @Override getLastSelectionWeight()268 public double getLastSelectionWeight() { 269 return mLastSelectionWeight; 270 } 271 272 @Override isCurrentNetwork()273 public boolean isCurrentNetwork() { 274 return mIsCurrentNetwork; 275 } 276 277 @Override isCurrentBssid()278 public boolean isCurrentBssid() { 279 return mIsCurrentBssid; 280 } 281 282 @Override getScanRssi()283 public int getScanRssi() { 284 return mScanRssi; 285 } 286 287 @Override getFrequency()288 public int getFrequency() { 289 return mFrequency; 290 } 291 292 @Override getPredictedThroughputMbps()293 public int getPredictedThroughputMbps() { 294 return mPredictedThroughputMbps; 295 } 296 297 @Override getEstimatedPercentInternetAvailability()298 public int getEstimatedPercentInternetAvailability() { 299 return mEstimatedPercentInternetAvailability; 300 } 301 302 /** 303 * Accesses statistical information from the score card 304 */ 305 @Override getEventStatistics(WifiScoreCardProto.Event event)306 public WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event) { 307 if (mPerBssid == null) return null; 308 WifiScoreCard.PerSignal perSignal = mPerBssid.lookupSignal(event, getFrequency()); 309 if (perSignal == null) return null; 310 return perSignal.toSignal(); 311 } 312 313 @Override toString()314 public String toString() { 315 Key key = getKey(); 316 String lastSelectionWeightString = ""; 317 if (getLastSelectionWeight() != 0.0) { 318 // Round this to 3 places 319 lastSelectionWeightString = "lastSelectionWeight = " 320 + Math.round(getLastSelectionWeight() * 1000.0) / 1000.0 321 + ", "; 322 } 323 return "Candidate { " 324 + "config = " + getNetworkConfigId() + ", " 325 + "bssid = " + key.bssid + ", " 326 + "freq = " + getFrequency() + ", " 327 + "rssi = " + getScanRssi() + ", " 328 + "Mbps = " + getPredictedThroughputMbps() + ", " 329 + "nominator = " + getNominatorId() + ", " 330 + "pInternet = " + getEstimatedPercentInternetAvailability() + ", " 331 + lastSelectionWeightString 332 + (isCurrentBssid() ? "connected, " : "") 333 + (isCurrentNetwork() ? "current, " : "") 334 + (isEphemeral() ? "ephemeral" : "saved") + ", " 335 + (isTrusted() ? "trusted, " : "") 336 + (isCarrierOrPrivileged() ? "priv, " : "") 337 + (isMetered() ? "metered, " : "") 338 + (hasNoInternetAccess() ? "noInternet, " : "") 339 + (isNoInternetAccessExpected() ? "noInternetExpected, " : "") 340 + (isPasspoint() ? "passpoint, " : "") 341 + (isOpenNetwork() ? "open" : "secure") + " }"; 342 } 343 } 344 345 /** 346 * Represents a scoring function 347 */ 348 public interface CandidateScorer { 349 /** 350 * The scorer's name, and perhaps important parameterization/version. 351 */ getIdentifier()352 String getIdentifier(); 353 354 /** 355 * Calculates the best score for a collection of candidates. 356 */ scoreCandidates(@onNull Collection<Candidate> candidates)357 @Nullable ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> candidates); 358 359 } 360 361 /** 362 * Represents a candidate with a real-valued score, along with an error estimate. 363 * 364 * Larger values reflect more desirable candidates. The range is arbitrary, 365 * because scores generated by different sources are not compared with each 366 * other. 367 * 368 * The error estimate is on the same scale as the value, and should 369 * always be strictly positive. For instance, it might be the standard deviation. 370 */ 371 public static class ScoredCandidate { 372 public final double value; 373 public final double err; 374 public final Key candidateKey; 375 public final boolean userConnectChoiceOverride; ScoredCandidate(double value, double err, boolean userConnectChoiceOverride, Candidate candidate)376 public ScoredCandidate(double value, double err, boolean userConnectChoiceOverride, 377 Candidate candidate) { 378 this.value = value; 379 this.err = err; 380 this.candidateKey = (candidate == null) ? null : candidate.getKey(); 381 this.userConnectChoiceOverride = userConnectChoiceOverride; 382 } 383 /** 384 * Represents no score 385 */ 386 public static final ScoredCandidate NONE = 387 new ScoredCandidate(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 388 false, null); 389 } 390 391 /** 392 * The key used for tracking candidates, consisting of SSID, security type, BSSID, and network 393 * configuration id. 394 */ 395 // TODO (b/123014687) unify with similar classes in the framework 396 public static class Key { 397 public final ScanResultMatchInfo matchInfo; // Contains the SSID and security type 398 public final MacAddress bssid; 399 public final int networkId; // network configuration id 400 Key(ScanResultMatchInfo matchInfo, MacAddress bssid, int networkId)401 public Key(ScanResultMatchInfo matchInfo, 402 MacAddress bssid, 403 int networkId) { 404 this.matchInfo = matchInfo; 405 this.bssid = bssid; 406 this.networkId = networkId; 407 } 408 409 @Override equals(Object other)410 public boolean equals(Object other) { 411 if (!(other instanceof Key)) return false; 412 Key that = (Key) other; 413 return (this.matchInfo.equals(that.matchInfo) 414 && this.bssid.equals(that.bssid) 415 && this.networkId == that.networkId); 416 } 417 418 @Override hashCode()419 public int hashCode() { 420 return Objects.hash(matchInfo, bssid, networkId); 421 } 422 } 423 424 private final Map<Key, Candidate> mCandidates = new ArrayMap<>(); 425 426 private int mCurrentNetworkId = -1; 427 @Nullable private MacAddress mCurrentBssid = null; 428 429 /** 430 * Sets up information about the currently-connected network. 431 */ setCurrent(int currentNetworkId, String currentBssid)432 public void setCurrent(int currentNetworkId, String currentBssid) { 433 mCurrentNetworkId = currentNetworkId; 434 mCurrentBssid = null; 435 if (currentBssid == null) return; 436 try { 437 mCurrentBssid = MacAddress.fromString(currentBssid); 438 } catch (RuntimeException e) { 439 failWithException(e); 440 } 441 } 442 443 /** 444 * Adds a new candidate 445 * 446 * @return true if added or replaced, false otherwise 447 */ add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, double lastSelectionWeightBetweenZeroAndOne, boolean isMetered, int predictedThroughputMbps)448 public boolean add(ScanDetail scanDetail, 449 WifiConfiguration config, 450 @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, 451 double lastSelectionWeightBetweenZeroAndOne, 452 boolean isMetered, 453 int predictedThroughputMbps) { 454 Key key = keyFromScanDetailAndConfig(scanDetail, config); 455 if (key == null) return false; 456 return add(key, config, nominatorId, 457 scanDetail.getScanResult().level, 458 scanDetail.getScanResult().frequency, 459 lastSelectionWeightBetweenZeroAndOne, 460 isMetered, 461 false, 462 predictedThroughputMbps); 463 } 464 465 /** 466 * Makes a Key from a ScanDetail and WifiConfiguration (null if error). 467 */ keyFromScanDetailAndConfig(ScanDetail scanDetail, WifiConfiguration config)468 public @Nullable Key keyFromScanDetailAndConfig(ScanDetail scanDetail, 469 WifiConfiguration config) { 470 if (!validConfigAndScanDetail(config, scanDetail)) return null; 471 ScanResult scanResult = scanDetail.getScanResult(); 472 MacAddress bssid = MacAddress.fromString(scanResult.BSSID); 473 return new Key(ScanResultMatchInfo.fromScanResult(scanResult), bssid, config.networkId); 474 } 475 476 /** 477 * Adds a new candidate 478 * 479 * @return true if added or replaced, false otherwise 480 */ add(@onNull Key key, WifiConfiguration config, @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, int scanRssi, int frequency, double lastSelectionWeightBetweenZeroAndOne, boolean isMetered, boolean isCarrierOrPrivileged, int predictedThroughputMbps)481 public boolean add(@NonNull Key key, 482 WifiConfiguration config, 483 @WifiNetworkSelector.NetworkNominator.NominatorId int nominatorId, 484 int scanRssi, 485 int frequency, 486 double lastSelectionWeightBetweenZeroAndOne, 487 boolean isMetered, 488 boolean isCarrierOrPrivileged, 489 int predictedThroughputMbps) { 490 Candidate old = mCandidates.get(key); 491 if (old != null) { 492 // check if we want to replace this old candidate 493 if (nominatorId > old.getNominatorId()) return false; 494 remove(old); 495 } 496 WifiScoreCard.PerBssid perBssid = mWifiScoreCard.lookupBssid( 497 key.matchInfo.networkSsid, 498 key.bssid.toString()); 499 perBssid.setSecurityType( 500 WifiScoreCardProto.SecurityType.forNumber(key.matchInfo.networkType)); 501 perBssid.setNetworkConfigId(config.networkId); 502 CandidateImpl candidate = new CandidateImpl(key, config, perBssid, nominatorId, 503 scanRssi, 504 frequency, 505 Math.min(Math.max(lastSelectionWeightBetweenZeroAndOne, 0.0), 1.0), 506 config.networkId == mCurrentNetworkId, 507 key.bssid.equals(mCurrentBssid), 508 isMetered, 509 isCarrierOrPrivileged, 510 predictedThroughputMbps); 511 mCandidates.put(key, candidate); 512 return true; 513 } 514 515 /** 516 * Checks that the supplied config and scan detail are valid (for the parts 517 * we care about) and consistent with each other. 518 * 519 * @param config to be validated 520 * @param scanDetail to be validated 521 * @return true if the config and scanDetail are consistent with each other 522 */ validConfigAndScanDetail(WifiConfiguration config, ScanDetail scanDetail)523 private boolean validConfigAndScanDetail(WifiConfiguration config, ScanDetail scanDetail) { 524 if (config == null) return failure(); 525 if (scanDetail == null) return failure(); 526 ScanResult scanResult = scanDetail.getScanResult(); 527 if (scanResult == null) return failure(); 528 MacAddress bssid; 529 try { 530 bssid = MacAddress.fromString(scanResult.BSSID); 531 } catch (RuntimeException e) { 532 return failWithException(e); 533 } 534 ScanResultMatchInfo key1 = ScanResultMatchInfo.fromScanResult(scanResult); 535 if (!config.isPasspoint()) { 536 ScanResultMatchInfo key2 = ScanResultMatchInfo.fromWifiConfiguration(config); 537 if (!key1.matchForNetworkSelection(key2, mContext.getResources() 538 .getBoolean(R.bool.config_wifiSaeUpgradeEnabled))) { 539 return failure(key1, key2); 540 } 541 } 542 return true; 543 } 544 545 /** 546 * Removes a candidate 547 * @return true if the candidate was successfully removed 548 */ remove(Candidate candidate)549 public boolean remove(Candidate candidate) { 550 if (!(candidate instanceof CandidateImpl)) return failure(); 551 return mCandidates.remove(candidate.getKey(), candidate); 552 } 553 554 /** 555 * Returns the number of candidates (at the BSSID level) 556 */ size()557 public int size() { 558 return mCandidates.size(); 559 } 560 561 /** 562 * Returns the candidates, grouped by network. 563 */ getGroupedCandidates()564 public Collection<Collection<Candidate>> getGroupedCandidates() { 565 Map<Integer, Collection<Candidate>> candidatesForNetworkId = new ArrayMap<>(); 566 for (Candidate candidate : mCandidates.values()) { 567 Collection<Candidate> cc = candidatesForNetworkId.get(candidate.getNetworkConfigId()); 568 if (cc == null) { 569 cc = new ArrayList<>(2); // Guess 2 bssids per network 570 candidatesForNetworkId.put(candidate.getNetworkConfigId(), cc); 571 } 572 cc.add(candidate); 573 } 574 return candidatesForNetworkId.values(); 575 } 576 577 /** 578 * Return a copy of the Candidates. 579 */ getCandidates()580 public List<Candidate> getCandidates() { 581 return mCandidates.entrySet().stream().map(entry -> entry.getValue()) 582 .collect(Collectors.toList()); 583 } 584 585 /** 586 * Make a choice from among the candidates, using the provided scorer. 587 * 588 * @return the chosen scored candidate, or ScoredCandidate.NONE. 589 */ choose(@onNull CandidateScorer candidateScorer)590 public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) { 591 Preconditions.checkNotNull(candidateScorer); 592 Collection<Candidate> candidates = new ArrayList<>(mCandidates.values()); 593 ScoredCandidate choice = candidateScorer.scoreCandidates(candidates); 594 return choice == null ? ScoredCandidate.NONE : choice; 595 } 596 597 /** 598 * After a failure indication is returned, this may be used to get details. 599 */ getLastFault()600 public RuntimeException getLastFault() { 601 return mLastFault; 602 } 603 604 /** 605 * Returns the number of faults we have seen 606 */ getFaultCount()607 public int getFaultCount() { 608 return mFaultCount; 609 } 610 611 /** 612 * Clears any recorded faults 613 */ clearFaults()614 public void clearFaults() { 615 mLastFault = null; 616 mFaultCount = 0; 617 } 618 619 /** 620 * Controls whether to immediately raise an exception on a failure 621 */ setPicky(boolean picky)622 public WifiCandidates setPicky(boolean picky) { 623 mPicky = picky; 624 return this; 625 } 626 627 /** 628 * Records details about a failure 629 * 630 * This captures a stack trace, so don't bother to construct a string message, just 631 * supply any culprits (convertible to strings) that might aid diagnosis. 632 * 633 * @return false 634 * @throws RuntimeException (if in picky mode) 635 */ failure(Object... culprits)636 private boolean failure(Object... culprits) { 637 StringJoiner joiner = new StringJoiner(","); 638 for (Object c : culprits) { 639 joiner.add("" + c); 640 } 641 return failWithException(new IllegalArgumentException(joiner.toString())); 642 } 643 644 /** 645 * As above, if we already have an exception. 646 */ failWithException(RuntimeException e)647 private boolean failWithException(RuntimeException e) { 648 mLastFault = e; 649 mFaultCount++; 650 if (mPicky) { 651 throw e; 652 } 653 return false; 654 } 655 656 private boolean mPicky = false; 657 private RuntimeException mLastFault = null; 658 private int mFaultCount = 0; 659 } 660