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