1 /*
2  * Copyright (C) 2016 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 static android.net.wifi.WifiManager.WIFI_FEATURE_OWE;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.net.NetworkKey;
26 import android.net.wifi.ScanResult;
27 import android.net.wifi.SupplicantState;
28 import android.net.wifi.WifiConfiguration;
29 import android.net.wifi.WifiInfo;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.util.ArraySet;
33 import android.util.LocalLog;
34 import android.util.Log;
35 import android.util.Pair;
36 
37 import com.android.internal.R;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.util.Preconditions;
40 import com.android.server.wifi.nano.WifiMetricsProto;
41 import com.android.server.wifi.util.ScanResultUtil;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Map;
50 
51 /**
52  * This class looks at all the connectivity scan results then
53  * selects a network for the phone to connect or roam to.
54  */
55 public class WifiNetworkSelector {
56     private static final String TAG = "WifiNetworkSelector";
57 
58     private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
59 
60     /**
61      * Minimum time gap between last successful network selection and a
62      * new selection attempt.
63      */
64     @VisibleForTesting
65     public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
66 
67     /**
68      * For this duration after user selected it, consider the current network as sufficient.
69      *
70      * This delays network selection during the time that connectivity service may be posting
71      * a dialog about a no-internet network.
72      */
73     @VisibleForTesting
74     public static final int LAST_USER_SELECTION_SUFFICIENT_MS = 30_000;
75 
76     /**
77      * Time that it takes for the boost given to the most recently user-selected
78      * network to decay to zero.
79      *
80      * In milliseconds.
81      */
82     @VisibleForTesting
83     public static final int LAST_USER_SELECTION_DECAY_TO_ZERO_MS = 8 * 60 * 60 * 1000;
84 
85     /**
86      * Connected score value used to decide whether a still-connected wifi should be treated
87      * as unconnected when filtering scan results.
88      */
89     @VisibleForTesting
90     public static final int WIFI_POOR_SCORE = ConnectedScore.WIFI_TRANSITION_SCORE - 10;
91 
92     /**
93      * The identifier string of the CandidateScorer to use (in the absence of overrides).
94      */
95     public static final String PRESET_CANDIDATE_SCORER_NAME = "CompatibilityScorer";
96 
97     /**
98      * Experiment ID for the legacy scorer.
99      */
100     public static final int LEGACY_CANDIDATE_SCORER_EXP_ID = 0;
101 
102     private final WifiConfigManager mWifiConfigManager;
103     private final Clock mClock;
104     private final LocalLog mLocalLog;
105     private final WifiMetrics mWifiMetrics;
106     private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
107     // Buffer of filtered scan results (Scan results considered by network selection) & associated
108     // WifiConfiguration (if any).
109     private final List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
110             new ArrayList<>();
111     private List<ScanDetail> mFilteredNetworks = new ArrayList<>();
112     private final WifiScoreCard mWifiScoreCard;
113     private final ScoringParams mScoringParams;
114     private final int mStayOnNetworkMinimumTxRate;
115     private final int mStayOnNetworkMinimumRxRate;
116     private final boolean mEnableAutoJoinWhenAssociated;
117     private final WifiNative mWifiNative;
118 
119     private final Map<String, WifiCandidates.CandidateScorer> mCandidateScorers = new ArrayMap<>();
120     private boolean mIsEnhancedOpenSupportedInitialized = false;
121     private boolean mIsEnhancedOpenSupported;
122 
123     /**
124      * WiFi Network Selector supports various categories of networks. Each category
125      * has an evaluator to choose the best WiFi network to connect to. Evaluators
126      * should be registered in order, by decreasing importance.
127      * Wifi Network Selector iterates through the registered scorers in registration order
128      * before making a final selection from among the candidates.
129      */
130 
131     /**
132      * Interface for WiFi Network Evaluator
133      *
134      * A network evaluator examines the scan results and recommends the
135      * best network in its category to connect to; it also reports the
136      * connectable candidates in its category for further consideration.
137      */
138     public interface NetworkEvaluator {
139         /** Type of evaluators */
140         int EVALUATOR_ID_SAVED = 0;
141         int EVALUATOR_ID_SUGGESTION = 1;
142         int EVALUATOR_ID_PASSPOINT = 2;
143         int EVALUATOR_ID_CARRIER = 3;
144         int EVALUATOR_ID_SCORED = 4;
145 
146         @IntDef(prefix = { "EVALUATOR_ID_" }, value = {
147                 EVALUATOR_ID_SAVED,
148                 EVALUATOR_ID_SUGGESTION,
149                 EVALUATOR_ID_PASSPOINT,
150                 EVALUATOR_ID_CARRIER,
151                 EVALUATOR_ID_SCORED})
152         @Retention(RetentionPolicy.SOURCE)
153         public @interface EvaluatorId {}
154 
155         /**
156          * Get the evaluator type.
157          */
getId()158         @EvaluatorId int getId();
159 
160         /**
161          * Get the evaluator name.
162          */
getName()163         String getName();
164 
165         /**
166          * Update the evaluator.
167          *
168          * Certain evaluators have to be updated with the new scan results. For example
169          * the ScoredNetworkEvaluator needs to refresh its Score Cache.
170          *
171          * @param scanDetails    a list of scan details constructed from the scan results
172          */
update(List<ScanDetail> scanDetails)173         void update(List<ScanDetail> scanDetails);
174 
175         /**
176          * Evaluate all the networks from the scan results.
177          *
178          * @param scanDetails    a list of scan details constructed from the scan results
179          * @param currentNetwork configuration of the current connected network
180          *                       or null if disconnected
181          * @param currentBssid   BSSID of the current connected network or null if
182          *                       disconnected
183          * @param connected      a flag to indicate if ClientModeImpl is in connected
184          *                       state
185          * @param untrustedNetworkAllowed a flag to indicate if untrusted networks like
186          *                                ephemeral networks are allowed
187          * @param onConnectableListener callback to record all of the connectable networks
188          *
189          * @return configuration of the chosen network;
190          *         null if no network in this category is available.
191          */
192         @Nullable
evaluateNetworks(List<ScanDetail> scanDetails, WifiConfiguration currentNetwork, String currentBssid, boolean connected, boolean untrustedNetworkAllowed, OnConnectableListener onConnectableListener)193         WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
194                         WifiConfiguration currentNetwork, String currentBssid,
195                         boolean connected, boolean untrustedNetworkAllowed,
196                         OnConnectableListener onConnectableListener);
197 
198         /**
199          * Callback for recording connectable candidates
200          */
201         public interface OnConnectableListener {
202             /**
203              * Notes that an access point is an eligible connection candidate
204              *
205              * @param scanDetail describes the specific access point
206              * @param config is the WifiConfiguration for the network
207              * @param score is the score assigned by the evaluator
208              */
onConnectable(ScanDetail scanDetail, WifiConfiguration config, int score)209             void onConnectable(ScanDetail scanDetail, WifiConfiguration config, int score);
210         }
211     }
212 
213     private final List<NetworkEvaluator> mEvaluators = new ArrayList<>(3);
214 
215     // A helper to log debugging information in the local log buffer, which can
216     // be retrieved in bugreport.
localLog(String log)217     private void localLog(String log) {
218         mLocalLog.log(log);
219     }
220 
isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails)221     private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails) {
222         // Currently connected?
223         if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) {
224             localLog("No current connected network.");
225             return false;
226         } else {
227             localLog("Current connected network: " + wifiInfo.getSSID()
228                     + " , ID: " + wifiInfo.getNetworkId());
229         }
230 
231         int currentRssi = wifiInfo.getRssi();
232         boolean hasQualifiedRssi = currentRssi
233                 > mScoringParams.getSufficientRssi(wifiInfo.getFrequency());
234         boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate)
235                 || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate);
236         if (hasQualifiedRssi && hasActiveStream) {
237             localLog("Stay on current network because of good RSSI and ongoing traffic");
238             return true;
239         }
240         WifiConfiguration network =
241                 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
242 
243         if (network == null) {
244             localLog("Current network was removed.");
245             return false;
246         }
247 
248         if (mWifiConfigManager.getLastSelectedNetwork() == network.networkId
249                 && (mClock.getElapsedSinceBootMillis()
250                     - mWifiConfigManager.getLastSelectedTimeStamp())
251                 <= LAST_USER_SELECTION_SUFFICIENT_MS) {
252             localLog("Current network is recently user-selected.");
253             return true;
254         }
255 
256         // OSU (Online Sign Up) network for Passpoint Release 2 is sufficient network.
257         if (network.osu) {
258             return true;
259         }
260 
261         // Ephemeral network is not qualified.
262         if (wifiInfo.isEphemeral()) {
263             localLog("Current network is an ephemeral one.");
264             return false;
265         }
266 
267         if (wifiInfo.is24GHz()) {
268             // 2.4GHz networks is not qualified whenever 5GHz is available
269             if (is5GHzNetworkAvailable(scanDetails)) {
270                 localLog("Current network is 2.4GHz. 5GHz networks available.");
271                 return false;
272             }
273         }
274         if (!hasQualifiedRssi) {
275             localLog("Current network RSSI[" + currentRssi + "]-acceptable but not qualified.");
276             return false;
277         }
278 
279         // Open network is not qualified.
280         if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
281             localLog("Current network is a open one.");
282             return false;
283         }
284 
285         // Network with no internet access reports is not qualified.
286         if (network.numNoInternetAccessReports > 0 && !network.noInternetAccessExpected) {
287             localLog("Current network has [" + network.numNoInternetAccessReports
288                     + "] no-internet access reports.");
289             return false;
290         }
291         return true;
292     }
293 
294     // Determine whether there are any 5GHz networks in the scan result
is5GHzNetworkAvailable(List<ScanDetail> scanDetails)295     private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) {
296         for (ScanDetail detail : scanDetails) {
297             ScanResult result = detail.getScanResult();
298             if (result.is5GHz()) return true;
299         }
300         return false;
301     }
302 
isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, boolean connected, boolean disconnected)303     private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
304                         boolean connected, boolean disconnected) {
305         if (scanDetails.size() == 0) {
306             localLog("Empty connectivity scan results. Skip network selection.");
307             return false;
308         }
309 
310         if (connected) {
311             // Is roaming allowed?
312             if (!mEnableAutoJoinWhenAssociated) {
313                 localLog("Switching networks in connected state is not allowed."
314                         + " Skip network selection.");
315                 return false;
316             }
317 
318             // Has it been at least the minimum interval since last network selection?
319             if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
320                 long gap = mClock.getElapsedSinceBootMillis()
321                             - mLastNetworkSelectionTimeStamp;
322                 if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) {
323                     localLog("Too short since last network selection: " + gap + " ms."
324                             + " Skip network selection.");
325                     return false;
326                 }
327             }
328 
329             if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) {
330                 localLog("Current connected network already sufficient. Skip network selection.");
331                 return false;
332             } else {
333                 localLog("Current connected network is not sufficient.");
334                 return true;
335             }
336         } else if (disconnected) {
337             return true;
338         } else {
339             // No network selection if ClientModeImpl is in a state other than
340             // CONNECTED or DISCONNECTED.
341             localLog("ClientModeImpl is in neither CONNECTED nor DISCONNECTED state."
342                     + " Skip network selection.");
343             return false;
344         }
345     }
346 
347     /**
348      * Format the given ScanResult as a scan ID for logging.
349      */
toScanId(@ullable ScanResult scanResult)350     public static String toScanId(@Nullable ScanResult scanResult) {
351         return scanResult == null ? "NULL"
352                                   : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
353     }
354 
355     /**
356      * Format the given WifiConfiguration as a SSID:netId string
357      */
toNetworkString(WifiConfiguration network)358     public static String toNetworkString(WifiConfiguration network) {
359         if (network == null) {
360             return null;
361         }
362 
363         return (network.SSID + ":" + network.networkId);
364     }
365 
366     /**
367      * Compares ScanResult level against the minimum threshold for its band, returns true if lower
368      */
isSignalTooWeak(ScanResult scanResult)369     public boolean isSignalTooWeak(ScanResult scanResult) {
370         return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency));
371     }
372 
filterScanResults(List<ScanDetail> scanDetails, HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid)373     private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails,
374                 HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) {
375         ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
376         List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
377         StringBuffer noValidSsid = new StringBuffer();
378         StringBuffer blacklistedBssid = new StringBuffer();
379         StringBuffer lowRssi = new StringBuffer();
380         boolean scanResultsHaveCurrentBssid = false;
381 
382         for (ScanDetail scanDetail : scanDetails) {
383             ScanResult scanResult = scanDetail.getScanResult();
384 
385             if (TextUtils.isEmpty(scanResult.SSID)) {
386                 noValidSsid.append(scanResult.BSSID).append(" / ");
387                 continue;
388             }
389 
390             // Check if the scan results contain the currently connected BSSID
391             if (scanResult.BSSID.equals(currentBssid)) {
392                 scanResultsHaveCurrentBssid = true;
393             }
394 
395             final String scanId = toScanId(scanResult);
396 
397             if (bssidBlacklist.contains(scanResult.BSSID)) {
398                 blacklistedBssid.append(scanId).append(" / ");
399                 continue;
400             }
401 
402             // Skip network with too weak signals.
403             if (isSignalTooWeak(scanResult)) {
404                 lowRssi.append(scanId).append("(")
405                     .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
406                     .append(")").append(scanResult.level).append(" / ");
407                 continue;
408             }
409 
410             validScanDetails.add(scanDetail);
411         }
412 
413         // WNS listens to all single scan results. Some scan requests may not include
414         // the channel of the currently connected network, so the currently connected
415         // network won't show up in the scan results. We don't act on these scan results
416         // to avoid aggressive network switching which might trigger disconnection.
417         if (isConnected && !scanResultsHaveCurrentBssid) {
418             localLog("Current connected BSSID " + currentBssid + " is not in the scan results."
419                     + " Skip network selection.");
420             validScanDetails.clear();
421             return validScanDetails;
422         }
423 
424         if (noValidSsid.length() != 0) {
425             localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
426         }
427 
428         if (blacklistedBssid.length() != 0) {
429             localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
430         }
431 
432         if (lowRssi.length() != 0) {
433             localLog("Networks filtered out due to low signal strength: " + lowRssi);
434         }
435 
436         return validScanDetails;
437     }
438 
isEnhancedOpenSupported()439     private boolean isEnhancedOpenSupported() {
440         if (mIsEnhancedOpenSupportedInitialized) {
441             return mIsEnhancedOpenSupported;
442         }
443 
444         mIsEnhancedOpenSupportedInitialized = true;
445         mIsEnhancedOpenSupported = (mWifiNative.getSupportedFeatureSet(
446                 mWifiNative.getClientInterfaceName()) & WIFI_FEATURE_OWE) != 0;
447         return mIsEnhancedOpenSupported;
448     }
449 
450     /**
451      * This returns a list of ScanDetails that were filtered in the process of network selection.
452      * The list is further filtered for only open unsaved networks.
453      *
454      * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS,
455      * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are
456      * no open unsaved networks, or when network selection has not been run.
457      */
getFilteredScanDetailsForOpenUnsavedNetworks()458     public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() {
459         List<ScanDetail> openUnsavedNetworks = new ArrayList<>();
460         boolean enhancedOpenSupported = isEnhancedOpenSupported();
461         for (ScanDetail scanDetail : mFilteredNetworks) {
462             ScanResult scanResult = scanDetail.getScanResult();
463 
464             if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
465                 continue;
466             }
467 
468             // Filter out Enhanced Open networks on devices that do not support it
469             if (ScanResultUtil.isScanResultForOweNetwork(scanResult)
470                     && !enhancedOpenSupported) {
471                 continue;
472             }
473 
474             // Skip saved networks
475             if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
476                 continue;
477             }
478 
479             openUnsavedNetworks.add(scanDetail);
480         }
481         return openUnsavedNetworks;
482     }
483 
484     /**
485      * This returns a list of ScanDetails that were filtered in the process of network selection.
486      * The list is further filtered for only carrier unsaved networks with EAP encryption.
487      *
488      * @param carrierConfig CarrierNetworkConfig used to filter carrier networks
489      * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS,
490      * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an
491      * empty list when there are no such networks, or when network selection has not been run.
492      */
getFilteredScanDetailsForCarrierUnsavedNetworks( CarrierNetworkConfig carrierConfig)493     public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks(
494             CarrierNetworkConfig carrierConfig) {
495         List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>();
496         for (ScanDetail scanDetail : mFilteredNetworks) {
497             ScanResult scanResult = scanDetail.getScanResult();
498 
499             if (!ScanResultUtil.isScanResultForEapNetwork(scanResult)
500                     || !carrierConfig.isCarrierNetwork(scanResult.SSID)) {
501                 continue;
502             }
503 
504             // Skip saved networks
505             if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
506                 continue;
507             }
508 
509             carrierUnsavedNetworks.add(scanDetail);
510         }
511         return carrierUnsavedNetworks;
512     }
513 
514     /**
515      * @return the list of ScanDetails scored as potential candidates by the last run of
516      * selectNetwork, this will be empty if Network selector determined no selection was
517      * needed on last run. This includes scan details of sufficient signal strength, and
518      * had an associated WifiConfiguration.
519      */
getConnectableScanDetails()520     public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() {
521         return mConnectableNetworks;
522     }
523 
524     /**
525      * This API is called when user explicitly selects a network. Currently, it is used in following
526      * cases:
527      * (1) User explicitly chooses to connect to a saved network.
528      * (2) User saves a network after adding a new network.
529      * (3) User saves a network after modifying a saved network.
530      * Following actions will be triggered:
531      * 1. If this network is disabled, we need re-enable it again.
532      * 2. This network is favored over all the other networks visible in latest network
533      *    selection procedure.
534      *
535      * @param netId  ID for the network chosen by the user
536      * @return true -- There is change made to connection choice of any saved network.
537      *         false -- There is no change made to connection choice of any saved network.
538      */
setUserConnectChoice(int netId)539     public boolean setUserConnectChoice(int netId) {
540         localLog("userSelectNetwork: network ID=" + netId);
541         WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
542 
543         if (selected == null || selected.SSID == null) {
544             localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
545             return false;
546         }
547 
548         // Enable the network if it is disabled.
549         if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
550             mWifiConfigManager.updateNetworkSelectionStatus(netId,
551                     WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
552         }
553         return setLegacyUserConnectChoice(selected);
554     }
555 
556     /**
557      * This maintains the legacy user connect choice state in the config store
558      */
setLegacyUserConnectChoice(@onNull final WifiConfiguration selected)559     private boolean setLegacyUserConnectChoice(@NonNull final WifiConfiguration selected) {
560         boolean change = false;
561         String key = selected.configKey();
562         // This is only used for setting the connect choice timestamp for debugging purposes.
563         long currentTime = mClock.getWallClockMillis();
564         List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks();
565 
566         for (WifiConfiguration network : configuredNetworks) {
567             WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
568             if (network.networkId == selected.networkId) {
569                 if (status.getConnectChoice() != null) {
570                     localLog("Remove user selection preference of " + status.getConnectChoice()
571                             + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
572                             + network.SSID + " : " + network.networkId);
573                     mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
574                     change = true;
575                 }
576                 continue;
577             }
578 
579             if (status.getSeenInLastQualifiedNetworkSelection()
580                         && !key.equals(status.getConnectChoice())) {
581                 localLog("Add key: " + key + " Set Time: " + currentTime + " to "
582                         + toNetworkString(network));
583                 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
584                 change = true;
585             }
586         }
587 
588         return change;
589     }
590 
591 
592     /**
593      * Iterate thru the list of configured networks (includes all saved network configurations +
594      * any ephemeral network configurations created for passpoint networks, suggestions, carrier
595      * networks, etc) and do the following:
596      * a) Try to re-enable any temporarily enabled networks (if the blacklist duration has expired).
597      * b) Clear the {@link WifiConfiguration.NetworkSelectionStatus#getCandidate()} field for all
598      * of them to identify networks that are present in the current scan result.
599      * c) Log any disabled networks.
600      */
updateConfiguredNetworks()601     private void updateConfiguredNetworks() {
602         List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks();
603         if (configuredNetworks.size() == 0) {
604             localLog("No configured networks.");
605             return;
606         }
607 
608         StringBuffer sbuf = new StringBuffer();
609         for (WifiConfiguration network : configuredNetworks) {
610             // If a configuration is temporarily disabled, re-enable it before trying
611             // to connect to it.
612             mWifiConfigManager.tryEnableNetwork(network.networkId);
613             // Clear the cached candidate, score and seen.
614             mWifiConfigManager.clearNetworkCandidateScanResult(network.networkId);
615 
616             // Log disabled network.
617             WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
618             if (!status.isNetworkEnabled()) {
619                 sbuf.append("  ").append(toNetworkString(network)).append(" ");
620                 for (int index = WifiConfiguration.NetworkSelectionStatus
621                         .NETWORK_SELECTION_DISABLED_STARTING_INDEX;
622                         index < WifiConfiguration.NetworkSelectionStatus
623                                 .NETWORK_SELECTION_DISABLED_MAX;
624                         index++) {
625                     int count = status.getDisableReasonCounter(index);
626                     // Here we log the reason as long as its count is greater than zero. The
627                     // network may not be disabled because of this particular reason. Logging
628                     // this information anyway to help understand what happened to the network.
629                     if (count > 0) {
630                         sbuf.append("reason=")
631                                 .append(WifiConfiguration.NetworkSelectionStatus
632                                         .getNetworkDisableReasonString(index))
633                                 .append(", count=").append(count).append("; ");
634                     }
635                 }
636                 sbuf.append("\n");
637             }
638         }
639 
640         if (sbuf.length() > 0) {
641             localLog("Disabled configured networks:");
642             localLog(sbuf.toString());
643         }
644     }
645 
646     /**
647      * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
648      * {@link WifiConfiguration} if one exists.
649      *
650      * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
651      */
overrideCandidateWithUserConnectChoice( @onNull WifiConfiguration candidate)652     private WifiConfiguration overrideCandidateWithUserConnectChoice(
653             @NonNull WifiConfiguration candidate) {
654         WifiConfiguration tempConfig = Preconditions.checkNotNull(candidate);
655         WifiConfiguration originalCandidate = candidate;
656         ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
657 
658         while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
659             String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
660             tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
661 
662             if (tempConfig != null) {
663                 WifiConfiguration.NetworkSelectionStatus tempStatus =
664                         tempConfig.getNetworkSelectionStatus();
665                 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
666                     scanResultCandidate = tempStatus.getCandidate();
667                     candidate = tempConfig;
668                 }
669             } else {
670                 localLog("Connect choice: " + key + " has no corresponding saved config.");
671                 break;
672             }
673         }
674 
675         if (candidate != originalCandidate) {
676             localLog("After user selection adjustment, the final candidate is:"
677                     + WifiNetworkSelector.toNetworkString(candidate) + " : "
678                     + scanResultCandidate.BSSID);
679             mWifiMetrics.setNominatorForNetwork(candidate.networkId,
680                     WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED_USER_CONNECT_CHOICE);
681         }
682         return candidate;
683     }
684 
685     /**
686      * Select the best network from the ones in range.
687      *
688      * @param scanDetails    List of ScanDetail for all the APs in range
689      * @param bssidBlacklist Blacklisted BSSIDs
690      * @param wifiInfo       Currently connected network
691      * @param connected      True if the device is connected
692      * @param disconnected   True if the device is disconnected
693      * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection
694      * @return Configuration of the selected network, or Null if nothing
695      */
696     @Nullable
selectNetwork(List<ScanDetail> scanDetails, HashSet<String> bssidBlacklist, WifiInfo wifiInfo, boolean connected, boolean disconnected, boolean untrustedNetworkAllowed)697     public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
698             HashSet<String> bssidBlacklist, WifiInfo wifiInfo,
699             boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
700         mFilteredNetworks.clear();
701         mConnectableNetworks.clear();
702         if (scanDetails.size() == 0) {
703             localLog("Empty connectivity scan result");
704             return null;
705         }
706 
707         WifiConfiguration currentNetwork =
708                 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
709 
710         // Always get the current BSSID from WifiInfo in case that firmware initiated
711         // roaming happened.
712         String currentBssid = wifiInfo.getBSSID();
713 
714         // Shall we start network selection at all?
715         if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
716             return null;
717         }
718 
719         // Update all configured networks before initiating network selection.
720         updateConfiguredNetworks();
721 
722         // Update the registered network evaluators.
723         for (NetworkEvaluator registeredEvaluator : mEvaluators) {
724             registeredEvaluator.update(scanDetails);
725         }
726 
727         // Filter out unwanted networks.
728         mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist,
729                 connected && wifiInfo.score >= WIFI_POOR_SCORE, currentBssid);
730         if (mFilteredNetworks.size() == 0) {
731             return null;
732         }
733 
734         // Determine the weight for the last user selection
735         final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork();
736         final double lastSelectionWeight = calculateLastSelectionWeight();
737         final ArraySet<Integer> mNetworkIds = new ArraySet<>();
738 
739         // Go through the registered network evaluators in order
740         WifiConfiguration selectedNetwork = null;
741         WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard);
742         if (currentNetwork != null) {
743             wifiCandidates.setCurrent(currentNetwork.networkId, currentBssid);
744         }
745         for (NetworkEvaluator registeredEvaluator : mEvaluators) {
746             localLog("About to run " + registeredEvaluator.getName() + " :");
747             WifiConfiguration choice = registeredEvaluator.evaluateNetworks(
748                     new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected,
749                     untrustedNetworkAllowed,
750                     (scanDetail, config, score) -> {
751                         if (config != null) {
752                             mConnectableNetworks.add(Pair.create(scanDetail, config));
753                             mNetworkIds.add(config.networkId);
754                             if (config.networkId == lastUserSelectedNetworkId) {
755                                 wifiCandidates.add(scanDetail, config,
756                                         registeredEvaluator.getId(), score, lastSelectionWeight);
757                             } else {
758                                 wifiCandidates.add(scanDetail, config,
759                                         registeredEvaluator.getId(), score);
760                             }
761                             mWifiMetrics.setNominatorForNetwork(config.networkId,
762                                     evaluatorIdToNominatorId(registeredEvaluator.getId()));
763                         }
764                     });
765             if (choice != null && !mNetworkIds.contains(choice.networkId)) {
766                 Log.wtf(TAG, registeredEvaluator.getName()
767                         + " failed to report choice with noConnectibleListener");
768             }
769             if (selectedNetwork == null && choice != null) {
770                 selectedNetwork = choice; // First one wins
771                 localLog(registeredEvaluator.getName() + " selects "
772                         + WifiNetworkSelector.toNetworkString(selectedNetwork));
773             }
774         }
775 
776         if (mConnectableNetworks.size() != wifiCandidates.size()) {
777             localLog("Connectable: " + mConnectableNetworks.size()
778                     + " Candidates: " + wifiCandidates.size());
779         }
780 
781         // Update the NetworkSelectionStatus in the configs for the current candidates
782         // This is needed for the legacy user connect choice, at least
783         Collection<Collection<WifiCandidates.Candidate>> groupedCandidates =
784                 wifiCandidates.getGroupedCandidates();
785         for (Collection<WifiCandidates.Candidate> group: groupedCandidates) {
786             WifiCandidates.Candidate best = null;
787             for (WifiCandidates.Candidate candidate: group) {
788                 // Of all the candidates with the same networkId, choose the
789                 // one with the smallest evaluatorId, and break ties by
790                 // picking the one with the highest score.
791                 if (best == null
792                         || candidate.getEvaluatorId() < best.getEvaluatorId()
793                         || (candidate.getEvaluatorId() == best.getEvaluatorId()
794                             && candidate.getEvaluatorScore() > best.getEvaluatorScore())) {
795                     best = candidate;
796                 }
797             }
798             if (best != null) {
799                 ScanDetail scanDetail = best.getScanDetail();
800                 if (scanDetail != null) {
801                     mWifiConfigManager.setNetworkCandidateScanResult(best.getNetworkConfigId(),
802                             scanDetail.getScanResult(), best.getEvaluatorScore());
803                 }
804             }
805         }
806 
807         ArrayMap<Integer, Integer> experimentNetworkSelections = new ArrayMap<>(); // for metrics
808 
809         final int legacySelectedNetworkId = selectedNetwork == null
810                 ? WifiConfiguration.INVALID_NETWORK_ID
811                 : selectedNetwork.networkId;
812 
813         int selectedNetworkId = legacySelectedNetworkId;
814 
815         // Run all the CandidateScorers
816         boolean legacyOverrideWanted = true;
817         final WifiCandidates.CandidateScorer activeScorer = getActiveCandidateScorer();
818         for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) {
819             WifiCandidates.ScoredCandidate choice;
820             try {
821                 choice = wifiCandidates.choose(candidateScorer);
822             } catch (RuntimeException e) {
823                 Log.wtf(TAG, "Exception running a CandidateScorer", e);
824                 continue;
825             }
826             int networkId = choice.candidateKey == null
827                     ? WifiConfiguration.INVALID_NETWORK_ID
828                     : choice.candidateKey.networkId;
829             String chooses = " would choose ";
830             if (candidateScorer == activeScorer) {
831                 chooses = " chooses ";
832                 legacyOverrideWanted = candidateScorer.userConnectChoiceOverrideWanted();
833                 selectedNetworkId = networkId;
834             }
835             String id = candidateScorer.getIdentifier();
836             int expid = experimentIdFromIdentifier(id);
837             localLog(id + chooses + networkId
838                     + " score " + choice.value + "+/-" + choice.err
839                     + " expid " + expid);
840             experimentNetworkSelections.put(expid, networkId);
841         }
842 
843         // Update metrics about differences in the selections made by various methods
844         final int activeExperimentId = activeScorer == null ? LEGACY_CANDIDATE_SCORER_EXP_ID
845                 : experimentIdFromIdentifier(activeScorer.getIdentifier());
846         experimentNetworkSelections.put(LEGACY_CANDIDATE_SCORER_EXP_ID, legacySelectedNetworkId);
847         for (Map.Entry<Integer, Integer> entry :
848                 experimentNetworkSelections.entrySet()) {
849             int experimentId = entry.getKey();
850             if (experimentId == activeExperimentId) continue;
851             int thisSelectedNetworkId = entry.getValue();
852             mWifiMetrics.logNetworkSelectionDecision(experimentId, activeExperimentId,
853                     selectedNetworkId == thisSelectedNetworkId,
854                     groupedCandidates.size());
855         }
856 
857         // Get a fresh copy of WifiConfiguration reflecting any scan result updates
858         selectedNetwork = mWifiConfigManager.getConfiguredNetwork(selectedNetworkId);
859         if (selectedNetwork != null && legacyOverrideWanted) {
860             selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
861             mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
862         }
863         return selectedNetwork;
864     }
865 
evaluatorIdToNominatorId(@etworkEvaluator.EvaluatorId int evaluatorId)866     private static int evaluatorIdToNominatorId(@NetworkEvaluator.EvaluatorId int evaluatorId) {
867         switch (evaluatorId) {
868             case NetworkEvaluator.EVALUATOR_ID_SAVED:
869                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED;
870             case NetworkEvaluator.EVALUATOR_ID_SUGGESTION:
871                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SUGGESTION;
872             case NetworkEvaluator.EVALUATOR_ID_PASSPOINT:
873                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_PASSPOINT;
874             case NetworkEvaluator.EVALUATOR_ID_CARRIER:
875                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_CARRIER;
876             case NetworkEvaluator.EVALUATOR_ID_SCORED:
877                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_EXTERNAL_SCORED;
878             default:
879                 Log.e(TAG, "UnrecognizedEvaluatorId" + evaluatorId);
880                 return WifiMetricsProto.ConnectionEvent.NOMINATOR_UNKNOWN;
881         }
882     }
883 
calculateLastSelectionWeight()884     private double calculateLastSelectionWeight() {
885         final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork();
886         double lastSelectionWeight = 0.0;
887         if (lastUserSelectedNetworkId != WifiConfiguration.INVALID_NETWORK_ID) {
888             double timeDifference = mClock.getElapsedSinceBootMillis()
889                     - mWifiConfigManager.getLastSelectedTimeStamp();
890             double unclipped = 1.0 - (timeDifference / LAST_USER_SELECTION_DECAY_TO_ZERO_MS);
891             lastSelectionWeight = Math.min(Math.max(unclipped, 0.0), 1.0);
892         }
893         return lastSelectionWeight;
894     }
895 
getActiveCandidateScorer()896     private WifiCandidates.CandidateScorer getActiveCandidateScorer() {
897         WifiCandidates.CandidateScorer ans = mCandidateScorers.get(PRESET_CANDIDATE_SCORER_NAME);
898         int overrideExperimentId = mScoringParams.getExperimentIdentifier();
899         if (overrideExperimentId >= MIN_SCORER_EXP_ID) {
900             for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) {
901                 int expId = experimentIdFromIdentifier(candidateScorer.getIdentifier());
902                 if (expId == overrideExperimentId) {
903                     ans = candidateScorer;
904                     break;
905                 }
906             }
907         }
908         if (ans == null && PRESET_CANDIDATE_SCORER_NAME != null) {
909             Log.wtf(TAG, PRESET_CANDIDATE_SCORER_NAME + " is not registered!");
910         }
911         mWifiMetrics.setNetworkSelectorExperimentId(ans == null
912                 ? LEGACY_CANDIDATE_SCORER_EXP_ID
913                 : experimentIdFromIdentifier(ans.getIdentifier()));
914         return ans;
915     }
916 
917     /**
918      * Register a network evaluator
919      *
920      * @param evaluator the network evaluator to be registered
921      *
922      */
registerNetworkEvaluator(@onNull NetworkEvaluator evaluator)923     public void registerNetworkEvaluator(@NonNull NetworkEvaluator evaluator) {
924         mEvaluators.add(Preconditions.checkNotNull(evaluator));
925     }
926 
927     /**
928      * Register a candidate scorer.
929      *
930      * Replaces any existing scorer having the same identifier.
931      */
registerCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)932     public void registerCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) {
933         String name = Preconditions.checkNotNull(candidateScorer).getIdentifier();
934         if (name != null) {
935             mCandidateScorers.put(name, candidateScorer);
936         }
937     }
938 
939     /**
940      * Unregister a candidate scorer.
941      */
unregisterCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)942     public void unregisterCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) {
943         String name = Preconditions.checkNotNull(candidateScorer).getIdentifier();
944         if (name != null) {
945             mCandidateScorers.remove(name);
946         }
947     }
948 
949     /**
950      * Derives a numeric experiment identifier from a CandidateScorer's identifier.
951      *
952      * @returns a positive number that starts with the decimal digits ID_PREFIX
953      */
experimentIdFromIdentifier(String id)954     public static int experimentIdFromIdentifier(String id) {
955         final int digits = (int) (((long) id.hashCode()) & Integer.MAX_VALUE) % ID_SUFFIX_MOD;
956         return ID_PREFIX * ID_SUFFIX_MOD + digits;
957     }
958     private static final int ID_SUFFIX_MOD = 1_000_000;
959     private static final int ID_PREFIX = 42;
960     private static final int MIN_SCORER_EXP_ID = ID_PREFIX * ID_SUFFIX_MOD;
961 
WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, WifiConfigManager configManager, Clock clock, LocalLog localLog, WifiMetrics wifiMetrics, WifiNative wifiNative)962     WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams,
963             WifiConfigManager configManager, Clock clock, LocalLog localLog,
964             WifiMetrics wifiMetrics, WifiNative wifiNative) {
965         mWifiConfigManager = configManager;
966         mClock = clock;
967         mWifiScoreCard = wifiScoreCard;
968         mScoringParams = scoringParams;
969         mLocalLog = localLog;
970         mWifiMetrics = wifiMetrics;
971         mWifiNative = wifiNative;
972 
973         mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
974                 R.bool.config_wifi_framework_enable_associated_network_selection);
975         mStayOnNetworkMinimumTxRate = context.getResources().getInteger(
976                 R.integer.config_wifi_framework_min_tx_rate_for_staying_on_network);
977         mStayOnNetworkMinimumRxRate = context.getResources().getInteger(
978                 R.integer.config_wifi_framework_min_rx_rate_for_staying_on_network);
979     }
980 }
981