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