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