• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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