• 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.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