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