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.net.MacAddress;
22 import android.net.wifi.ScanResult;
23 import android.net.wifi.WifiConfiguration;
24 import android.util.ArrayMap;
25 
26 import com.android.internal.util.Preconditions;
27 
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.StringJoiner;
33 
34 /**
35  * Candidates for network selection
36  */
37 public class WifiCandidates {
38     private static final String TAG = "WifiCandidates";
39 
WifiCandidates(@onNull WifiScoreCard wifiScoreCard)40     WifiCandidates(@NonNull WifiScoreCard wifiScoreCard) {
41         mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard);
42     }
43     private final WifiScoreCard mWifiScoreCard;
44 
45     /**
46      * Represents a connectable candidate.
47      */
48     public interface Candidate {
49         /**
50          * Gets the Key, which contains the SSID, BSSID, security type, and config id.
51          *
52          * Generally, a CandidateScorer should not need to use this.
53          */
getKey()54         @Nullable Key getKey();
55         /**
56          * Gets the ScanDetail associate with the candidate.
57          */
getScanDetail()58         @Nullable ScanDetail getScanDetail();
59         /**
60          * Gets the config id.
61          */
getNetworkConfigId()62         int getNetworkConfigId();
63         /**
64          * Returns true for an open network.
65          */
isOpenNetwork()66         boolean isOpenNetwork();
67         /**
68          * Returns true for a passpoint network.
69          */
isPasspoint()70         boolean isPasspoint();
71         /**
72          * Returns true for an ephemeral network.
73          */
isEphemeral()74         boolean isEphemeral();
75         /**
76          * Returns true for a trusted network.
77          */
isTrusted()78         boolean isTrusted();
79         /**
80          * Returns the ID of the evaluator that provided the candidate.
81          */
getEvaluatorId()82         @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId();
83         /**
84          * Gets the score that was provided by the evaluator.
85          *
86          * Not all evaluators provide a useful score. Scores from different evaluators
87          * are not directly comparable.
88          */
getEvaluatorScore()89         int getEvaluatorScore();
90         /**
91          * Returns true if the candidate is in the same network as the
92          * current connection.
93          */
isCurrentNetwork()94         boolean isCurrentNetwork();
95         /**
96          * Return true if the candidate is currently connected.
97          */
isCurrentBssid()98         boolean isCurrentBssid();
99         /**
100          * Returns a value between 0 and 1.
101          *
102          * 1.0 means the network was recently selected by the user or an app.
103          * 0.0 means not recently selected by user or app.
104          */
getLastSelectionWeight()105         double getLastSelectionWeight();
106         /**
107          * Gets the scan RSSI.
108          */
getScanRssi()109         int getScanRssi();
110         /**
111          * Gets the scan frequency.
112          */
getFrequency()113         int getFrequency();
114         /**
115          * Gets statistics from the scorecard.
116          */
getEventStatistics(WifiScoreCardProto.Event event)117         @Nullable WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event);
118     }
119 
120     /**
121      * Represents a connectable candidate
122      */
123     static class CandidateImpl implements Candidate {
124         public final Key key;                   // SSID/sectype/BSSID/configId
125         public final ScanDetail scanDetail;
126         public final WifiConfiguration config;
127         // First evaluator to nominate this config
128         public final @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId;
129         public final int evaluatorScore;        // Score provided by first nominating evaluator
130         public final double lastSelectionWeight; // Value between 0 and 1
131 
132         private WifiScoreCard.PerBssid mPerBssid; // For accessing the scorecard entry
133         private final boolean mIsCurrentNetwork;
134         private final boolean mIsCurrentBssid;
135 
CandidateImpl(Key key, ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, WifiScoreCard.PerBssid perBssid, double lastSelectionWeight, boolean isCurrentNetwork, boolean isCurrentBssid)136         CandidateImpl(Key key,
137                 ScanDetail scanDetail,
138                 WifiConfiguration config,
139                 @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
140                 int evaluatorScore,
141                 WifiScoreCard.PerBssid perBssid,
142                 double lastSelectionWeight,
143                 boolean isCurrentNetwork,
144                 boolean isCurrentBssid) {
145             this.key = key;
146             this.scanDetail = scanDetail;
147             this.config = config;
148             this.evaluatorId = evaluatorId;
149             this.evaluatorScore = evaluatorScore;
150             this.mPerBssid = perBssid;
151             this.lastSelectionWeight = lastSelectionWeight;
152             this.mIsCurrentNetwork = isCurrentNetwork;
153             this.mIsCurrentBssid = isCurrentBssid;
154         }
155 
156         @Override
getKey()157         public Key getKey() {
158             return key;
159         }
160 
161         @Override
getNetworkConfigId()162         public int getNetworkConfigId() {
163             return key.networkId;
164         }
165 
166         @Override
getScanDetail()167         public ScanDetail getScanDetail() {
168             return scanDetail;
169         }
170 
171         @Override
isOpenNetwork()172         public boolean isOpenNetwork() {
173             // TODO - should be able to base this on key.matchInfo.securityType
174             return WifiConfigurationUtil.isConfigForOpenNetwork(config);
175         }
176 
177         @Override
isPasspoint()178         public boolean isPasspoint() {
179             return config.isPasspoint();
180         }
181 
182         @Override
isEphemeral()183         public boolean isEphemeral() {
184             return config.ephemeral;
185         }
186 
187         @Override
isTrusted()188         public boolean isTrusted() {
189             return config.trusted;
190         }
191 
192         @Override
getEvaluatorId()193         public @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId() {
194             return evaluatorId;
195         }
196 
197         @Override
getEvaluatorScore()198         public int getEvaluatorScore() {
199             return evaluatorScore;
200         }
201 
202         @Override
getLastSelectionWeight()203         public double getLastSelectionWeight() {
204             return lastSelectionWeight;
205         }
206 
207         @Override
isCurrentNetwork()208         public boolean isCurrentNetwork() {
209             return mIsCurrentNetwork;
210         }
211 
212         @Override
isCurrentBssid()213         public boolean isCurrentBssid() {
214             return mIsCurrentBssid;
215         }
216 
217         @Override
getScanRssi()218         public int getScanRssi() {
219             return scanDetail.getScanResult().level;
220         }
221 
222         @Override
getFrequency()223         public int getFrequency() {
224             return scanDetail.getScanResult().frequency;
225         }
226 
227         /**
228          * Accesses statistical information from the score card
229          */
230         @Override
231         public WifiScoreCardProto.Signal
getEventStatistics(WifiScoreCardProto.Event event)232                 getEventStatistics(WifiScoreCardProto.Event event) {
233             if (mPerBssid == null) return null;
234             WifiScoreCard.PerSignal perSignal = mPerBssid.lookupSignal(event, getFrequency());
235             if (perSignal == null) return null;
236             return perSignal.toSignal();
237         }
238 
239     }
240 
241     /**
242      * Represents a scoring function
243      */
244     public interface CandidateScorer {
245         /**
246          * The scorer's name, and perhaps important parameterization/version.
247          */
getIdentifier()248         String getIdentifier();
249 
250         /**
251          * Calculates the score for a group of candidates that belong
252          * to the same network.
253          */
scoreCandidates(@onNull Collection<Candidate> group)254         @Nullable ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> group);
255 
256         /**
257          * Returns true if the legacy user connect choice logic should be used.
258          *
259          * @returns false to disable the legacy logic
260          */
userConnectChoiceOverrideWanted()261         boolean userConnectChoiceOverrideWanted();
262     }
263 
264     /**
265      * Represents a candidate with a real-valued score, along with an error estimate.
266      *
267      * Larger values reflect more desirable candidates. The range is arbitrary,
268      * because scores generated by different sources are not compared with each
269      * other.
270      *
271      * The error estimate is on the same scale as the value, and should
272      * always be strictly positive. For instance, it might be the standard deviation.
273      */
274     public static class ScoredCandidate {
275         public final double value;
276         public final double err;
277         public final Key candidateKey;
ScoredCandidate(double value, double err, Candidate candidate)278         public ScoredCandidate(double value, double err, Candidate candidate) {
279             this.value = value;
280             this.err = err;
281             this.candidateKey = (candidate == null) ? null : candidate.getKey();
282         }
283         /**
284          * Represents no score
285          */
286         public static final ScoredCandidate NONE =
287                 new ScoredCandidate(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, null);
288     }
289 
290     /**
291      * The key used for tracking candidates, consisting of SSID, security type, BSSID, and network
292      * configuration id.
293      */
294     // TODO (b/123014687) unify with similar classes in the framework
295     public static class Key {
296         public final ScanResultMatchInfo matchInfo; // Contains the SSID and security type
297         public final MacAddress bssid;
298         public final int networkId;                 // network configuration id
299 
Key(ScanResultMatchInfo matchInfo, MacAddress bssid, int networkId)300         public Key(ScanResultMatchInfo matchInfo,
301                    MacAddress bssid,
302                    int networkId) {
303             this.matchInfo = matchInfo;
304             this.bssid = bssid;
305             this.networkId = networkId;
306         }
307 
308         @Override
equals(Object other)309         public boolean equals(Object other) {
310             if (!(other instanceof Key)) return false;
311             Key that = (Key) other;
312             return (this.matchInfo.equals(that.matchInfo)
313                     && this.bssid.equals(that.bssid)
314                     && this.networkId == that.networkId);
315         }
316 
317         @Override
hashCode()318         public int hashCode() {
319             return Objects.hash(matchInfo, bssid, networkId);
320         }
321     }
322 
323     private final Map<Key, CandidateImpl> mCandidates = new ArrayMap<>();
324 
325     private int mCurrentNetworkId = -1;
326     @Nullable private MacAddress mCurrentBssid = null;
327 
328     /**
329      * Sets up information about the currently-connected network.
330      */
setCurrent(int currentNetworkId, String currentBssid)331     public void setCurrent(int currentNetworkId, String currentBssid) {
332         mCurrentNetworkId = currentNetworkId;
333         mCurrentBssid = null;
334         if (currentBssid == null) return;
335         try {
336             mCurrentBssid = MacAddress.fromString(currentBssid);
337         } catch (RuntimeException e) {
338             failWithException(e);
339         }
340     }
341 
342     /**
343      * Adds a new candidate
344      *
345      * @returns true if added or replaced, false otherwise
346      */
add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, double lastSelectionWeightBetweenZeroAndOne)347     public boolean add(ScanDetail scanDetail,
348                     WifiConfiguration config,
349                     @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
350                     int evaluatorScore,
351                     double lastSelectionWeightBetweenZeroAndOne) {
352         if (config == null) return failure();
353         if (scanDetail == null) return failure();
354         ScanResult scanResult = scanDetail.getScanResult();
355         if (scanResult == null) return failure();
356         MacAddress bssid;
357         try {
358             bssid = MacAddress.fromString(scanResult.BSSID);
359         } catch (RuntimeException e) {
360             return failWithException(e);
361         }
362         ScanResultMatchInfo key1 = ScanResultMatchInfo.fromWifiConfiguration(config);
363         ScanResultMatchInfo key2 = ScanResultMatchInfo.fromScanResult(scanResult);
364         if (!key1.equals(key2)) return failure(key1, key2);
365         Key key = new Key(key1, bssid, config.networkId);
366         CandidateImpl old = mCandidates.get(key);
367         if (old != null) {
368             // check if we want to replace this old candidate
369             if (evaluatorId < old.evaluatorId) return failure();
370             if (evaluatorId > old.evaluatorId) return false;
371             if (evaluatorScore <= old.evaluatorScore) return false;
372             remove(old);
373         }
374         WifiScoreCard.PerBssid perBssid = mWifiScoreCard.lookupBssid(
375                 key.matchInfo.networkSsid,
376                 key.bssid.toString());
377         perBssid.setSecurityType(
378                 WifiScoreCardProto.SecurityType.forNumber(key.matchInfo.networkType));
379         perBssid.setNetworkConfigId(config.networkId);
380         CandidateImpl candidate = new CandidateImpl(key,
381                 scanDetail, config, evaluatorId, evaluatorScore, perBssid,
382                 Math.min(Math.max(lastSelectionWeightBetweenZeroAndOne, 0.0), 1.0),
383                 config.networkId == mCurrentNetworkId,
384                 bssid.equals(mCurrentBssid));
385         mCandidates.put(key, candidate);
386         return true;
387     }
388     /** Adds a new candidate with no user selection weight. */
add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore)389     public boolean add(ScanDetail scanDetail,
390                     WifiConfiguration config,
391                     @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
392                     int evaluatorScore) {
393         return add(scanDetail, config, evaluatorId, evaluatorScore, 0.0);
394     }
395 
396     /**
397      * Removes a candidate
398      * @returns true if the candidate was successfully removed
399      */
remove(Candidate candidate)400     public boolean remove(Candidate candidate) {
401         if (!(candidate instanceof CandidateImpl)) return failure();
402         return mCandidates.remove(((CandidateImpl) candidate).key, (CandidateImpl) candidate);
403     }
404 
405     /**
406      * Returns the number of candidates (at the BSSID level)
407      */
size()408     public int size() {
409         return mCandidates.size();
410     }
411 
412     /**
413      * Returns the candidates, grouped by network.
414      */
getGroupedCandidates()415     public Collection<Collection<Candidate>> getGroupedCandidates() {
416         Map<Integer, Collection<Candidate>> candidatesForNetworkId = new ArrayMap<>();
417         for (CandidateImpl candidate : mCandidates.values()) {
418             Collection<Candidate> cc = candidatesForNetworkId.get(candidate.key.networkId);
419             if (cc == null) {
420                 cc = new ArrayList<>(2); // Guess 2 bssids per network
421                 candidatesForNetworkId.put(candidate.key.networkId, cc);
422             }
423             cc.add(candidate);
424         }
425         return candidatesForNetworkId.values();
426     }
427 
428     /**
429      * Make a choice from among the candidates, using the provided scorer.
430      *
431      * @returns the chosen scored candidate, or ScoredCandidate.NONE.
432      */
choose(@onNull CandidateScorer candidateScorer)433     public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) {
434         Preconditions.checkNotNull(candidateScorer);
435         ScoredCandidate choice = ScoredCandidate.NONE;
436         for (Collection<Candidate> group : getGroupedCandidates()) {
437             ScoredCandidate scoredCandidate = candidateScorer.scoreCandidates(group);
438             if (scoredCandidate != null && scoredCandidate.value > choice.value) {
439                 choice = scoredCandidate;
440             }
441         }
442         return choice;
443     }
444 
445     /**
446      * After a failure indication is returned, this may be used to get details.
447      */
getLastFault()448     public RuntimeException getLastFault() {
449         return mLastFault;
450     }
451 
452     /**
453      * Returns the number of faults we have seen
454      */
getFaultCount()455     public int getFaultCount() {
456         return mFaultCount;
457     }
458 
459     /**
460      * Clears any recorded faults
461      */
clearFaults()462     public void clearFaults() {
463         mLastFault = null;
464         mFaultCount = 0;
465     }
466 
467     /**
468      * Controls whether to immediately raise an exception on a failure
469      */
setPicky(boolean picky)470     public WifiCandidates setPicky(boolean picky) {
471         mPicky = picky;
472         return this;
473     }
474 
475     /**
476      * Records details about a failure
477      *
478      * This captures a stack trace, so don't bother to construct a string message, just
479      * supply any culprits (convertible to strings) that might aid diagnosis.
480      *
481      * @returns false
482      * @throws RuntimeException (if in picky mode)
483      */
failure(Object... culprits)484     private boolean failure(Object... culprits) {
485         StringJoiner joiner = new StringJoiner(",");
486         for (Object c : culprits) {
487             joiner.add("" + c);
488         }
489         return failWithException(new IllegalArgumentException(joiner.toString()));
490     }
491 
492     /**
493      * As above, if we already have an exception.
494      */
failWithException(RuntimeException e)495     private boolean failWithException(RuntimeException e) {
496         mLastFault = e;
497         mFaultCount++;
498         if (mPicky) {
499             throw e;
500         }
501         return false;
502     }
503 
504     private boolean mPicky = false;
505     private RuntimeException mLastFault = null;
506     private int mFaultCount = 0;
507 
508 }
509