1 /*
2  * Copyright (C) 2015 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.Nullable;
20 import android.content.Context;
21 import android.net.NetworkKey;
22 import android.net.NetworkScoreManager;
23 import android.net.WifiKey;
24 import android.net.wifi.ScanResult;
25 import android.net.wifi.WifiConfiguration;
26 import android.net.wifi.WifiInfo;
27 import android.net.wifi.WifiManager;
28 import android.text.TextUtils;
29 import android.util.LocalLog;
30 import android.util.Log;
31 import android.util.Pair;
32 
33 import com.android.internal.R;
34 import com.android.internal.annotations.VisibleForTesting;
35 
36 import java.io.FileDescriptor;
37 import java.io.PrintWriter;
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * This class looks at all the connectivity scan results then
48  * select an network for the phone to connect/roam to.
49  */
50 public class WifiQualifiedNetworkSelector {
51     private WifiConfigManager mWifiConfigManager;
52     private WifiInfo mWifiInfo;
53     private NetworkScoreManager mScoreManager;
54     private WifiNetworkScoreCache mNetworkScoreCache;
55     private Clock mClock;
56     private static final String TAG = "WifiQualifiedNetworkSelector:";
57     // Always enable debugging logs for now since QNS is still a new feature.
58     private static final boolean FORCE_DEBUG = true;
59     private boolean mDbg = FORCE_DEBUG;
60     private WifiConfiguration mCurrentConnectedNetwork = null;
61     private String mCurrentBssid = null;
62     //buffer most recent scan results
63     private List<ScanDetail> mScanDetails = null;
64     //buffer of filtered scan results (Scan results considered by network selection) & associated
65     //WifiConfiguration (if any)
66     private volatile List<Pair<ScanDetail, WifiConfiguration>> mFilteredScanDetails = null;
67 
68     //Minimum time gap between last successful Qualified Network Selection and new selection attempt
69     //usable only when current state is connected state   default 10 s
70     private static final int MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL = 10 * 1000;
71 
72     //if current network is on 2.4GHz band and has a RSSI over this, need not new network selection
73     public static final int QUALIFIED_RSSI_24G_BAND = -73;
74     //if current network is on 5GHz band and has a RSSI over this, need not new network selection
75     public static final int QUALIFIED_RSSI_5G_BAND = -70;
76     //any RSSI larger than this will benefit the traffic very limited
77     public static final int RSSI_SATURATION_2G_BAND = -60;
78     public static final int RSSI_SATURATION_5G_BAND = -57;
79     //Any value below this will be considered not usable
80     public static final int MINIMUM_2G_ACCEPT_RSSI = -85;
81     public static final int MINIMUM_5G_ACCEPT_RSSI = -82;
82 
83     public static final int RSSI_SCORE_SLOPE = 4;
84     public static final int RSSI_SCORE_OFFSET = 85;
85 
86     public static final int BAND_AWARD_5GHz = 40;
87     public static final int SAME_NETWORK_AWARD = 16;
88 
89     public static final int SAME_BSSID_AWARD = 24;
90     public static final int LAST_SELECTION_AWARD = 480;
91     public static final int PASSPOINT_SECURITY_AWARD = 40;
92     public static final int SECURITY_AWARD = 80;
93     public static final int BSSID_BLACKLIST_THRESHOLD = 3;
94     public static final int BSSID_BLACKLIST_EXPIRE_TIME = 5 * 60 * 1000;
95     private final int mNoIntnetPenalty;
96     //TODO: check whether we still need this one when we update the scan manager
97     public static final int SCAN_RESULT_MAXIMUNM_AGE = 40000;
98     private static final int INVALID_TIME_STAMP = -1;
99     private long mLastQualifiedNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
100 
101     // Temporarily, for dog food
102     private final LocalLog mLocalLog = new LocalLog(1024);
103     private int mRssiScoreSlope = RSSI_SCORE_SLOPE;
104     private int mRssiScoreOffset = RSSI_SCORE_OFFSET;
105     private int mSameBssidAward = SAME_BSSID_AWARD;
106     private int mLastSelectionAward = LAST_SELECTION_AWARD;
107     private int mPasspointSecurityAward = PASSPOINT_SECURITY_AWARD;
108     private int mSecurityAward = SECURITY_AWARD;
109     private int mUserPreferedBand = WifiManager.WIFI_FREQUENCY_BAND_AUTO;
110     private Map<String, BssidBlacklistStatus> mBssidBlacklist =
111             new HashMap<String, BssidBlacklistStatus>();
112 
113     /**
114      * class save the blacklist status of a given BSSID
115      */
116     private static class BssidBlacklistStatus {
117         //how many times it is requested to be blacklisted (association rejection trigger this)
118         int mCounter;
119         boolean mIsBlacklisted;
120         long mBlacklistedTimeStamp = INVALID_TIME_STAMP;
121     }
122 
localLog(String log)123     private void localLog(String log) {
124         if (mDbg) {
125             mLocalLog.log(log);
126         }
127     }
128 
localLoge(String log)129     private void localLoge(String log) {
130         mLocalLog.log(log);
131     }
132 
133     @VisibleForTesting
setWifiNetworkScoreCache(WifiNetworkScoreCache cache)134     void setWifiNetworkScoreCache(WifiNetworkScoreCache cache) {
135         mNetworkScoreCache = cache;
136     }
137 
138     /**
139      * @return current target connected network
140      */
getConnetionTargetNetwork()141     public WifiConfiguration getConnetionTargetNetwork() {
142         return mCurrentConnectedNetwork;
143     }
144 
145     /**
146      * @return the list of ScanDetails scored as potential candidates by the last run of
147      * selectQualifiedNetwork, this will be empty if QNS determined no selection was needed on last
148      * run. This includes scan details of sufficient signal strength, and had an associated
149      * WifiConfiguration.
150      */
getFilteredScanDetails()151     public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() {
152         return mFilteredScanDetails;
153     }
154 
155     /**
156      * set the user selected preferred band
157      *
158      * @param band preferred band user selected
159      */
setUserPreferredBand(int band)160     public void setUserPreferredBand(int band) {
161         mUserPreferedBand = band;
162     }
163 
WifiQualifiedNetworkSelector(WifiConfigManager configureStore, Context context, WifiInfo wifiInfo, Clock clock)164     WifiQualifiedNetworkSelector(WifiConfigManager configureStore, Context context,
165             WifiInfo wifiInfo, Clock clock) {
166         mWifiConfigManager = configureStore;
167         mWifiInfo = wifiInfo;
168         mClock = clock;
169         mScoreManager =
170                 (NetworkScoreManager) context.getSystemService(Context.NETWORK_SCORE_SERVICE);
171         if (mScoreManager != null) {
172             mNetworkScoreCache = new WifiNetworkScoreCache(context);
173             mScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache);
174         } else {
175             localLoge("No network score service: Couldn't register as a WiFi score Manager, type="
176                     + NetworkKey.TYPE_WIFI + " service= " + Context.NETWORK_SCORE_SERVICE);
177             mNetworkScoreCache = null;
178         }
179 
180         mRssiScoreSlope = context.getResources().getInteger(
181                 R.integer.config_wifi_framework_RSSI_SCORE_SLOPE);
182         mRssiScoreOffset = context.getResources().getInteger(
183                 R.integer.config_wifi_framework_RSSI_SCORE_OFFSET);
184         mSameBssidAward = context.getResources().getInteger(
185                 R.integer.config_wifi_framework_SAME_BSSID_AWARD);
186         mLastSelectionAward = context.getResources().getInteger(
187                 R.integer.config_wifi_framework_LAST_SELECTION_AWARD);
188         mPasspointSecurityAward = context.getResources().getInteger(
189                 R.integer.config_wifi_framework_PASSPOINT_SECURITY_AWARD);
190         mSecurityAward = context.getResources().getInteger(
191                 R.integer.config_wifi_framework_SECURITY_AWARD);
192         mNoIntnetPenalty = (mWifiConfigManager.mThresholdSaturatedRssi24.get() + mRssiScoreOffset)
193                 * mRssiScoreSlope + mWifiConfigManager.mBandAward5Ghz.get()
194                 + mWifiConfigManager.mCurrentNetworkBoost.get() + mSameBssidAward + mSecurityAward;
195     }
196 
enableVerboseLogging(int verbose)197     void enableVerboseLogging(int verbose) {
198         mDbg = verbose > 0 || FORCE_DEBUG;
199     }
200 
getNetworkString(WifiConfiguration network)201     private String getNetworkString(WifiConfiguration network) {
202         if (network == null) {
203             return null;
204         }
205 
206         return (network.SSID + ":" + network.networkId);
207 
208     }
209 
210     /**
211      * check whether current network is good enough we need not consider any potential switch
212      *
213      * @param currentNetwork -- current connected network
214      * @return true -- qualified and do not consider potential network switch
215      *         false -- not good enough and should try potential network switch
216      */
isNetworkQualified(WifiConfiguration currentNetwork)217     private boolean isNetworkQualified(WifiConfiguration currentNetwork) {
218 
219         if (currentNetwork == null) {
220             localLog("Disconnected");
221             return false;
222         } else {
223             localLog("Current network is: " + currentNetwork.SSID + " ,ID is: "
224                     + currentNetwork.networkId);
225         }
226 
227         //if current connected network is an ephemeral network,we will consider
228         // there is no current network
229         if (currentNetwork.ephemeral) {
230             localLog("Current is ephemeral. Start reselect");
231             return false;
232         }
233 
234         //if current network is open network, not qualified
235         if (mWifiConfigManager.isOpenNetwork(currentNetwork)) {
236             localLog("Current network is open network");
237             return false;
238         }
239 
240         // Current network band must match with user preference selection
241         if (mWifiInfo.is24GHz() && (mUserPreferedBand != WifiManager.WIFI_FREQUENCY_BAND_2GHZ)) {
242             localLog("Current band dose not match user preference. Start Qualified Network"
243                     + " Selection Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band"
244                     : "5GHz band") + "UserPreference band = " + mUserPreferedBand);
245             return false;
246         }
247 
248         int currentRssi = mWifiInfo.getRssi();
249         if ((mWifiInfo.is24GHz()
250                         && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi24.get())
251                 || (mWifiInfo.is5GHz()
252                         && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi5.get())) {
253             localLog("Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band" : "5GHz band")
254                     + "current RSSI is: " + currentRssi);
255             return false;
256         }
257 
258         return true;
259     }
260 
261     /**
262      * check whether QualifiedNetworkSelection is needed or not
263      *
264      * @param isLinkDebouncing true -- Link layer is under debouncing
265      *                         false -- Link layer is not under debouncing
266      * @param isConnected true -- device is connected to an AP currently
267      *                    false -- device is not connected to an AP currently
268      * @param isDisconnected true -- WifiStateMachine is at disconnected state
269      *                       false -- WifiStateMachine is not at disconnected state
270      * @param isSupplicantTransientState true -- supplicant is in a transient state now
271      *                                   false -- supplicant is not in a transient state now
272      * @return true -- need a Qualified Network Selection procedure
273      *         false -- do not need a QualifiedNetworkSelection procedure
274      */
needQualifiedNetworkSelection(boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected, boolean isSupplicantTransientState)275     private boolean needQualifiedNetworkSelection(boolean isLinkDebouncing, boolean isConnected,
276             boolean isDisconnected, boolean isSupplicantTransientState) {
277         if (mScanDetails.size() == 0) {
278             localLog("empty scan result");
279             return false;
280         }
281 
282         // Do not trigger Qualified Network Selection during L2 link debouncing procedure
283         if (isLinkDebouncing) {
284             localLog("Need not Qualified Network Selection during L2 debouncing");
285             return false;
286         }
287 
288         if (isConnected) {
289             //already connected. Just try to find better candidate
290             //if switch network is not allowed in connected mode, do not trigger Qualified Network
291             //Selection
292             if (!mWifiConfigManager.getEnableAutoJoinWhenAssociated()) {
293                 localLog("Switch network under connection is not allowed");
294                 return false;
295             }
296 
297             //Do not select again if last selection is within
298             //MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL
299             if (mLastQualifiedNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
300                 long gap = mClock.elapsedRealtime() - mLastQualifiedNetworkSelectionTimeStamp;
301                 if (gap < MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL) {
302                     localLog("Too short to last successful Qualified Network Selection Gap is:"
303                             + gap + " ms!");
304                     return false;
305                 }
306             }
307 
308             WifiConfiguration currentNetwork =
309                     mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId());
310             if (currentNetwork == null) {
311                 // WifiStateMachine in connected state but WifiInfo is not. It means there is a race
312                 // condition happened. Do not make QNS until WifiStateMachine goes into
313                 // disconnected state
314                 return false;
315             }
316 
317             if (!isNetworkQualified(mCurrentConnectedNetwork)) {
318                 //need not trigger Qualified Network Selection if current network is qualified
319                 localLog("Current network is not qualified");
320                 return true;
321             } else {
322                 return false;
323             }
324         } else if (isDisconnected) {
325             mCurrentConnectedNetwork = null;
326             mCurrentBssid = null;
327             //Do not start Qualified Network Selection if current state is a transient state
328             if (isSupplicantTransientState) {
329                 return false;
330             }
331         } else {
332             //Do not allow new network selection in other state
333             localLog("WifiStateMachine is not on connected or disconnected state");
334             return false;
335         }
336 
337         return true;
338     }
339 
calculateBssidScore(ScanResult scanResult, WifiConfiguration network, WifiConfiguration currentNetwork, boolean sameBssid, boolean sameSelect, StringBuffer sbuf)340     int calculateBssidScore(ScanResult scanResult, WifiConfiguration network,
341             WifiConfiguration currentNetwork, boolean sameBssid, boolean sameSelect,
342             StringBuffer sbuf) {
343 
344         int score = 0;
345         //calculate the RSSI score
346         int rssi = scanResult.level <= mWifiConfigManager.mThresholdSaturatedRssi24.get()
347                 ? scanResult.level : mWifiConfigManager.mThresholdSaturatedRssi24.get();
348         score += (rssi + mRssiScoreOffset) * mRssiScoreSlope;
349         sbuf.append(" RSSI score: " +  score);
350         if (scanResult.is5GHz()) {
351             //5GHz band
352             score += mWifiConfigManager.mBandAward5Ghz.get();
353             sbuf.append(" 5GHz bonus: " + mWifiConfigManager.mBandAward5Ghz.get());
354         }
355 
356         //last user selection award
357         if (sameSelect) {
358             long timeDifference = mClock.elapsedRealtime()
359                     - mWifiConfigManager.getLastSelectedTimeStamp();
360 
361             if (timeDifference > 0) {
362                 int bonus = mLastSelectionAward - (int) (timeDifference / 1000 / 60);
363                 score += bonus > 0 ? bonus : 0;
364                 sbuf.append(" User selected it last time " + (timeDifference / 1000 / 60)
365                         + " minutes ago, bonus:" + bonus);
366             }
367         }
368 
369         //same network award
370         if (network == currentNetwork || network.isLinked(currentNetwork)) {
371             score += mWifiConfigManager.mCurrentNetworkBoost.get();
372             sbuf.append(" Same network with current associated. Bonus: "
373                     + mWifiConfigManager.mCurrentNetworkBoost.get());
374         }
375 
376         //same BSSID award
377         if (sameBssid) {
378             score += mSameBssidAward;
379             sbuf.append(" Same BSSID with current association. Bonus: " + mSameBssidAward);
380         }
381 
382         //security award
383         if (network.isPasspoint()) {
384             score += mPasspointSecurityAward;
385             sbuf.append(" Passpoint Bonus:" + mPasspointSecurityAward);
386         } else if (!mWifiConfigManager.isOpenNetwork(network)) {
387             score += mSecurityAward;
388             sbuf.append(" Secure network Bonus:" + mSecurityAward);
389         }
390 
391         //Penalty for no internet network. Make sure if there is any network with Internet,
392         //however, if there is no any other network with internet, this network can be chosen
393         if (network.numNoInternetAccessReports > 0 && !network.validatedInternetAccess) {
394             score -= mNoIntnetPenalty;
395             sbuf.append(" No internet Penalty:-" + mNoIntnetPenalty);
396         }
397 
398 
399         sbuf.append(" Score for scanResult: " + scanResult +  " and Network ID: "
400                 + network.networkId + " final score:" + score + "\n\n");
401 
402         return score;
403     }
404 
405     /**
406      * This API try to update all the saved networks' network selection status
407      */
updateSavedNetworkSelectionStatus()408     private void updateSavedNetworkSelectionStatus() {
409         List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
410         if (savedNetworks.size() == 0) {
411             localLog("no saved network");
412             return;
413         }
414 
415         StringBuffer sbuf = new StringBuffer("Saved Network List\n");
416         for (WifiConfiguration network : savedNetworks) {
417             WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId);
418             WifiConfiguration.NetworkSelectionStatus status =
419                     config.getNetworkSelectionStatus();
420 
421             //If the configuration is temporarily disabled, try to re-enable it
422             if (status.isNetworkTemporaryDisabled()) {
423                 mWifiConfigManager.tryEnableQualifiedNetwork(network.networkId);
424             }
425 
426             //clean the cached candidate, score and seen
427             status.setCandidate(null);
428             status.setCandidateScore(Integer.MIN_VALUE);
429             status.setSeenInLastQualifiedNetworkSelection(false);
430 
431             //print the debug messages
432             sbuf.append("    " + getNetworkString(network) + " " + " User Preferred BSSID:"
433                     + network.BSSID + " FQDN:" + network.FQDN + " "
434                     + status.getNetworkStatusString() + " Disable account: ");
435             for (int index = status.NETWORK_SELECTION_ENABLE;
436                     index < status.NETWORK_SELECTION_DISABLED_MAX; index++) {
437                 sbuf.append(status.getDisableReasonCounter(index) + " ");
438             }
439             sbuf.append("Connect Choice:" + status.getConnectChoice() + " set time:"
440                     + status.getConnectChoiceTimestamp());
441             sbuf.append("\n");
442         }
443         localLog(sbuf.toString());
444     }
445 
446     /**
447      * This API is called when user explicitly select a network. Currently, it is used in following
448      * cases:
449      * (1) User explicitly choose to connect to a saved network
450      * (2) User save a network after add a new network
451      * (3) User save a network after modify a saved network
452      * Following actions will be triggered:
453      * 1. if this network is disabled, we need re-enable it again
454      * 2. we considered user prefer this network over all the networks visible in latest network
455      *    selection procedure
456      *
457      * @param netId new network ID for either the network the user choose or add
458      * @param persist whether user has the authority to overwrite current connect choice
459      * @return true -- There is change made to connection choice of any saved network
460      *         false -- There is no change made to connection choice of any saved network
461      */
userSelectNetwork(int netId, boolean persist)462     public boolean userSelectNetwork(int netId, boolean persist) {
463         WifiConfiguration selected = mWifiConfigManager.getWifiConfiguration(netId);
464         localLog("userSelectNetwork:" + netId + " persist:" + persist);
465         if (selected == null || selected.SSID == null) {
466             localLoge("userSelectNetwork: Bad configuration with nid=" + netId);
467             return false;
468         }
469 
470 
471         if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
472             mWifiConfigManager.updateNetworkSelectionStatus(netId,
473                     WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
474         }
475 
476         if (!persist) {
477             localLog("User has no privilege to overwrite the current priority");
478             return false;
479         }
480 
481         boolean change = false;
482         String key = selected.configKey();
483         // This is only used for setting the connect choice timestamp for debugging purposes.
484         long currentTime = mClock.currentTimeMillis();
485         List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
486 
487         for (WifiConfiguration network : savedNetworks) {
488             WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId);
489             WifiConfiguration.NetworkSelectionStatus status = config.getNetworkSelectionStatus();
490             if (config.networkId == selected.networkId) {
491                 if (status.getConnectChoice() != null) {
492                     localLog("Remove user selection preference of " + status.getConnectChoice()
493                             + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
494                             + config.SSID + " : " + config.networkId);
495                     status.setConnectChoice(null);
496                     status.setConnectChoiceTimestamp(WifiConfiguration.NetworkSelectionStatus
497                             .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
498                     change = true;
499                 }
500                 continue;
501             }
502 
503             if (status.getSeenInLastQualifiedNetworkSelection()
504                     && (status.getConnectChoice() == null
505                     || !status.getConnectChoice().equals(key))) {
506                 localLog("Add key:" + key + " Set Time: " + currentTime + " to "
507                         + getNetworkString(config));
508                 status.setConnectChoice(key);
509                 status.setConnectChoiceTimestamp(currentTime);
510                 change = true;
511             }
512         }
513         //Write this change to file
514         if (change) {
515             mWifiConfigManager.writeKnownNetworkHistory();
516             return true;
517         }
518 
519         return false;
520     }
521 
522     /**
523      * enable/disable a BSSID for Quality Network Selection
524      * When an association rejection event is obtained, Quality Network Selector will disable this
525      * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it
526      * successfully later, this bssid can be re-enabled.
527      *
528      * @param bssid the bssid to be enabled / disabled
529      * @param enable -- true enable a bssid if it has been disabled
530      *               -- false disable a bssid
531      */
enableBssidForQualityNetworkSelection(String bssid, boolean enable)532     public boolean enableBssidForQualityNetworkSelection(String bssid, boolean enable) {
533         if (enable) {
534             return (mBssidBlacklist.remove(bssid) != null);
535         } else {
536             if (bssid != null) {
537                 BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
538                 if (status == null) {
539                     //first time
540                     BssidBlacklistStatus newStatus = new BssidBlacklistStatus();
541                     newStatus.mCounter++;
542                     mBssidBlacklist.put(bssid, newStatus);
543                 } else if (!status.mIsBlacklisted) {
544                     status.mCounter++;
545                     if (status.mCounter >= BSSID_BLACKLIST_THRESHOLD) {
546                         status.mIsBlacklisted = true;
547                         status.mBlacklistedTimeStamp = mClock.elapsedRealtime();
548                         return true;
549                     }
550                 }
551             }
552         }
553         return false;
554     }
555 
556     /**
557      * update the buffered BSSID blacklist
558      *
559      * Go through the whole buffered BSSIDs blacklist and check when the BSSIDs is blocked. If they
560      * were blacked before BSSID_BLACKLIST_EXPIRE_TIME, re-enable it again.
561      */
updateBssidBlacklist()562     private void updateBssidBlacklist() {
563         Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator();
564         while (iter.hasNext()) {
565             BssidBlacklistStatus status = iter.next();
566             if (status != null && status.mIsBlacklisted) {
567                 if (mClock.elapsedRealtime() - status.mBlacklistedTimeStamp
568                             >= BSSID_BLACKLIST_EXPIRE_TIME) {
569                     iter.remove();
570                 }
571             }
572         }
573     }
574 
575     /**
576      * Check whether a bssid is disabled
577      * @param bssid -- the bssid to check
578      * @return true -- bssid is disabled
579      *         false -- bssid is not disabled
580      */
isBssidDisabled(String bssid)581     public boolean isBssidDisabled(String bssid) {
582         BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
583         return status == null ? false : status.mIsBlacklisted;
584     }
585 
586     /**
587      * ToDo: This should be called in Connectivity Manager when it gets new scan result
588      * check whether a network slection is needed. If need, check all the new scan results and
589      * select a new qualified network/BSSID to connect to
590      *
591      * @param forceSelectNetwork true -- start a qualified network selection anyway,no matter
592      *                           current network is already qualified or not.
593      *                           false -- if current network is already qualified, do not do new
594      *                           selection
595      * @param isUntrustedConnectionsAllowed true -- user allow to connect to untrusted network
596      *                                      false -- user do not allow to connect to untrusted
597      *                                      network
598      * @param scanDetails latest scan result obtained (should be connectivity scan only)
599      * @param isLinkDebouncing true -- Link layer is under debouncing
600      *                         false -- Link layer is not under debouncing
601      * @param isConnected true -- device is connected to an AP currently
602      *                    false -- device is not connected to an AP currently
603      * @param isDisconnected true -- WifiStateMachine is at disconnected state
604      *                       false -- WifiStateMachine is not at disconnected state
605      * @param isSupplicantTransient true -- supplicant is in a transient state
606      *                              false -- supplicant is not in a transient state
607      * @return the qualified network candidate found. If no available candidate, return null
608      */
selectQualifiedNetwork(boolean forceSelectNetwork , boolean isUntrustedConnectionsAllowed, List<ScanDetail> scanDetails, boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected, boolean isSupplicantTransient)609     public WifiConfiguration selectQualifiedNetwork(boolean forceSelectNetwork ,
610             boolean isUntrustedConnectionsAllowed, List<ScanDetail>  scanDetails,
611             boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected,
612             boolean isSupplicantTransient) {
613         localLog("==========start qualified Network Selection==========");
614         mScanDetails = scanDetails;
615         List<Pair<ScanDetail, WifiConfiguration>>  filteredScanDetails = new ArrayList<>();
616         if (mCurrentConnectedNetwork == null) {
617             mCurrentConnectedNetwork =
618                     mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId());
619         }
620 
621         if (mCurrentBssid == null) {
622             mCurrentBssid = mWifiInfo.getBSSID();
623         }
624 
625         if (!forceSelectNetwork && !needQualifiedNetworkSelection(isLinkDebouncing, isConnected,
626                 isDisconnected, isSupplicantTransient)) {
627             localLog("Quit qualified Network Selection since it is not forced and current network"
628                     + " is qualified already");
629             mFilteredScanDetails = filteredScanDetails;
630             return null;
631         }
632 
633         int currentHighestScore = Integer.MIN_VALUE;
634         ScanResult scanResultCandidate = null;
635         WifiConfiguration networkCandidate = null;
636         final ExternalScoreEvaluator externalScoreEvaluator =
637                 new ExternalScoreEvaluator(mLocalLog, mDbg);
638         String lastUserSelectedNetWorkKey = mWifiConfigManager.getLastSelectedConfiguration();
639         WifiConfiguration lastUserSelectedNetwork =
640                 mWifiConfigManager.getWifiConfiguration(lastUserSelectedNetWorkKey);
641         if (lastUserSelectedNetwork != null) {
642             localLog("Last selection is " + lastUserSelectedNetwork.SSID + " Time to now: "
643                     + ((mClock.elapsedRealtime() - mWifiConfigManager.getLastSelectedTimeStamp())
644                             / 1000 / 60 + " minutes"));
645         }
646 
647         updateSavedNetworkSelectionStatus();
648         updateBssidBlacklist();
649 
650         StringBuffer lowSignalScan = new StringBuffer();
651         StringBuffer notSavedScan = new StringBuffer();
652         StringBuffer noValidSsid = new StringBuffer();
653         StringBuffer scoreHistory =  new StringBuffer();
654         ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
655 
656         //iterate all scan results and find the best candidate with the highest score
657         for (ScanDetail scanDetail : mScanDetails) {
658             ScanResult scanResult = scanDetail.getScanResult();
659             //skip bad scan result
660             if (scanResult.SSID == null || TextUtils.isEmpty(scanResult.SSID)) {
661                 if (mDbg) {
662                     //We should not see this in ePNO
663                     noValidSsid.append(scanResult.BSSID + " / ");
664                 }
665                 continue;
666             }
667 
668             final String scanId = toScanId(scanResult);
669             //check whether this BSSID is blocked or not
670             if (mWifiConfigManager.isBssidBlacklisted(scanResult.BSSID)
671                     || isBssidDisabled(scanResult.BSSID)) {
672                 //We should not see this in ePNO
673                 Log.e(TAG, scanId + " is in blacklist.");
674                 continue;
675             }
676 
677             //skip scan result with too weak signals
678             if ((scanResult.is24GHz() && scanResult.level
679                     < mWifiConfigManager.mThresholdMinimumRssi24.get())
680                     || (scanResult.is5GHz() && scanResult.level
681                     < mWifiConfigManager.mThresholdMinimumRssi5.get())) {
682                 if (mDbg) {
683                     lowSignalScan.append(scanId + "(" + (scanResult.is24GHz() ? "2.4GHz" : "5GHz")
684                             + ")" + scanResult.level + " / ");
685                 }
686                 continue;
687             }
688 
689             //check if there is already a score for this network
690             if (mNetworkScoreCache != null && !mNetworkScoreCache.isScoredNetwork(scanResult)) {
691                 //no score for this network yet.
692                 WifiKey wifiKey;
693 
694                 try {
695                     wifiKey = new WifiKey("\"" + scanResult.SSID + "\"", scanResult.BSSID);
696                     NetworkKey ntwkKey = new NetworkKey(wifiKey);
697                     //add to the unscoredNetworks list so we can request score later
698                     unscoredNetworks.add(ntwkKey);
699                 } catch (IllegalArgumentException e) {
700                     Log.w(TAG, "Invalid SSID=" + scanResult.SSID + " BSSID=" + scanResult.BSSID
701                             + " for network score. Skip.");
702                 }
703             }
704 
705             //check whether this scan result belong to a saved network
706             boolean potentiallyEphemeral = false;
707             // Stores WifiConfiguration of potential connection candidates for scan result filtering
708             WifiConfiguration potentialEphemeralCandidate = null;
709             List<WifiConfiguration> associatedWifiConfigurations =
710                     mWifiConfigManager.updateSavedNetworkWithNewScanDetail(scanDetail,
711                             isSupplicantTransient || isConnected || isLinkDebouncing);
712             if (associatedWifiConfigurations == null) {
713                 potentiallyEphemeral =  true;
714                 if (mDbg) {
715                     notSavedScan.append(scanId + " / ");
716                 }
717             } else if (associatedWifiConfigurations.size() == 1) {
718                 //if there are more than 1 associated network, it must be a passpoint network
719                 WifiConfiguration network = associatedWifiConfigurations.get(0);
720                 if (network.ephemeral) {
721                     potentialEphemeralCandidate = network;
722                     potentiallyEphemeral =  true;
723                 }
724             }
725 
726             // Evaluate the potentially ephemeral network as a possible candidate if untrusted
727             // connections are allowed and we have an external score for the scan result.
728             if (potentiallyEphemeral) {
729                 if (isUntrustedConnectionsAllowed) {
730                     Integer netScore = getNetworkScore(scanResult, false);
731                     if (netScore != null
732                         && !mWifiConfigManager.wasEphemeralNetworkDeleted(scanResult.SSID)) {
733                         externalScoreEvaluator.evalUntrustedCandidate(netScore, scanResult);
734                         // scanDetail is for available ephemeral network
735                         filteredScanDetails.add(Pair.create(scanDetail,
736                                 potentialEphemeralCandidate));
737                     }
738                 }
739                 continue;
740             }
741 
742             // calculate the score of each scanresult whose associated network is not ephemeral. Due
743             // to one scan result can associated with more than 1 network, we need calculate all
744             // the scores and use the highest one as the scanresults score.
745             int highestScore = Integer.MIN_VALUE;
746             int score;
747             WifiConfiguration configurationCandidateForThisScan = null;
748             WifiConfiguration potentialCandidate = null;
749             for (WifiConfiguration network : associatedWifiConfigurations) {
750                 WifiConfiguration.NetworkSelectionStatus status =
751                         network.getNetworkSelectionStatus();
752                 status.setSeenInLastQualifiedNetworkSelection(true);
753                 if (potentialCandidate == null) {
754                     potentialCandidate = network;
755                 }
756                 if (!status.isNetworkEnabled()) {
757                     continue;
758                 } else if (network.BSSID != null && !network.BSSID.equals("any")
759                         && !network.BSSID.equals(scanResult.BSSID)) {
760                     //in such scenario, user (APP) has specified the only BSSID to connect for this
761                     // configuration. So only the matched scan result can be candidate
762                     localLog("Network: " + getNetworkString(network) + " has specified" + "BSSID:"
763                             + network.BSSID + ". Skip " + scanResult.BSSID);
764                     continue;
765                 }
766 
767                 // If the network is marked to use external scores then attempt to fetch the score.
768                 // These networks will not be considered alongside the other saved networks.
769                 if (network.useExternalScores) {
770                     Integer netScore = getNetworkScore(scanResult, false);
771                     externalScoreEvaluator.evalSavedCandidate(netScore, network, scanResult);
772                     continue;
773                 }
774 
775                 score = calculateBssidScore(scanResult, network, mCurrentConnectedNetwork,
776                         (mCurrentBssid == null ? false : mCurrentBssid.equals(scanResult.BSSID)),
777                         (lastUserSelectedNetwork == null ? false : lastUserSelectedNetwork.networkId
778                          == network.networkId), scoreHistory);
779                 if (score > highestScore) {
780                     highestScore = score;
781                     configurationCandidateForThisScan = network;
782                     potentialCandidate = network;
783                 }
784                 //update the cached candidate
785                 if (score > status.getCandidateScore()) {
786                     status.setCandidate(scanResult);
787                     status.setCandidateScore(score);
788                 }
789             }
790             // Create potential filteredScanDetail entry
791             filteredScanDetails.add(Pair.create(scanDetail, potentialCandidate));
792 
793             if (highestScore > currentHighestScore || (highestScore == currentHighestScore
794                     && scanResultCandidate != null
795                     && scanResult.level > scanResultCandidate.level)) {
796                 currentHighestScore = highestScore;
797                 scanResultCandidate = scanResult;
798                 networkCandidate = configurationCandidateForThisScan;
799             }
800         }
801 
802         mFilteredScanDetails = filteredScanDetails;
803 
804         //kick the score manager if there is any unscored network
805         if (mScoreManager != null && unscoredNetworks.size() != 0) {
806             NetworkKey[] unscoredNetworkKeys =
807                     unscoredNetworks.toArray(new NetworkKey[unscoredNetworks.size()]);
808             mScoreManager.requestScores(unscoredNetworkKeys);
809         }
810 
811         if (mDbg) {
812             localLog(lowSignalScan + " skipped due to low signal\n");
813             localLog(notSavedScan + " skipped due to not saved\n ");
814             localLog(noValidSsid + " skipped due to not valid SSID\n");
815             localLog(scoreHistory.toString());
816         }
817 
818         //we need traverse the whole user preference to choose the one user like most now
819         if (scanResultCandidate != null) {
820             WifiConfiguration tempConfig = networkCandidate;
821 
822             while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
823                 String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
824                 tempConfig = mWifiConfigManager.getWifiConfiguration(key);
825 
826                 if (tempConfig != null) {
827                     WifiConfiguration.NetworkSelectionStatus tempStatus =
828                             tempConfig.getNetworkSelectionStatus();
829                     if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
830                         scanResultCandidate = tempStatus.getCandidate();
831                         networkCandidate = tempConfig;
832                     }
833                 } else {
834                     //we should not come here in theory
835                     localLoge("Connect choice: " + key + " has no corresponding saved config");
836                     break;
837                 }
838             }
839             localLog("After user choice adjust, the final candidate is:"
840                     + getNetworkString(networkCandidate) + " : " + scanResultCandidate.BSSID);
841         }
842 
843         // At this point none of the saved networks were good candidates so we fall back to
844         // externally scored networks if any are available.
845         if (scanResultCandidate == null) {
846             localLog("Checking the externalScoreEvaluator for candidates...");
847             networkCandidate = getExternalScoreCandidate(externalScoreEvaluator);
848             if (networkCandidate != null) {
849                 scanResultCandidate = networkCandidate.getNetworkSelectionStatus().getCandidate();
850             }
851         }
852 
853         if (scanResultCandidate == null) {
854             localLog("Can not find any suitable candidates");
855             return null;
856         }
857 
858         String currentAssociationId = mCurrentConnectedNetwork == null ? "Disconnected" :
859                 getNetworkString(mCurrentConnectedNetwork);
860         String targetAssociationId = getNetworkString(networkCandidate);
861         //In passpoint, saved configuration has garbage SSID. We need update it with the SSID of
862         //the scan result.
863         if (networkCandidate.isPasspoint()) {
864             // This will update the passpoint configuration in WifiConfigManager
865             networkCandidate.SSID = "\"" + scanResultCandidate.SSID + "\"";
866         }
867 
868         //For debug purpose only
869         if (scanResultCandidate.BSSID.equals(mCurrentBssid)) {
870             localLog(currentAssociationId + " is already the best choice!");
871         } else if (mCurrentConnectedNetwork != null
872                 && (mCurrentConnectedNetwork.networkId == networkCandidate.networkId
873                 || mCurrentConnectedNetwork.isLinked(networkCandidate))) {
874             localLog("Roaming from " + currentAssociationId + " to " + targetAssociationId);
875         } else {
876             localLog("reconnect from " + currentAssociationId + " to " + targetAssociationId);
877         }
878 
879         mCurrentBssid = scanResultCandidate.BSSID;
880         mCurrentConnectedNetwork = networkCandidate;
881         mLastQualifiedNetworkSelectionTimeStamp = mClock.elapsedRealtime();
882         return networkCandidate;
883     }
884 
885     /**
886      * Returns the best candidate network according to the given ExternalScoreEvaluator.
887      */
888     @Nullable
getExternalScoreCandidate(ExternalScoreEvaluator scoreEvaluator)889     WifiConfiguration getExternalScoreCandidate(ExternalScoreEvaluator scoreEvaluator) {
890         WifiConfiguration networkCandidate = null;
891         switch (scoreEvaluator.getBestCandidateType()) {
892             case ExternalScoreEvaluator.BestCandidateType.UNTRUSTED_NETWORK:
893                 ScanResult untrustedScanResultCandidate =
894                         scoreEvaluator.getScanResultCandidate();
895                 WifiConfiguration unTrustedNetworkCandidate =
896                         mWifiConfigManager.wifiConfigurationFromScanResult(
897                                 untrustedScanResultCandidate);
898 
899                 // Mark this config as ephemeral so it isn't persisted.
900                 unTrustedNetworkCandidate.ephemeral = true;
901                 if (mNetworkScoreCache != null) {
902                     unTrustedNetworkCandidate.meteredHint =
903                             mNetworkScoreCache.getMeteredHint(untrustedScanResultCandidate);
904                 }
905                 mWifiConfigManager.saveNetwork(unTrustedNetworkCandidate,
906                         WifiConfiguration.UNKNOWN_UID);
907 
908                 localLog(String.format("new ephemeral candidate %s network ID:%d, "
909                                 + "meteredHint=%b",
910                         toScanId(untrustedScanResultCandidate), unTrustedNetworkCandidate.networkId,
911                         unTrustedNetworkCandidate.meteredHint));
912 
913                 unTrustedNetworkCandidate.getNetworkSelectionStatus()
914                         .setCandidate(untrustedScanResultCandidate);
915                 networkCandidate = unTrustedNetworkCandidate;
916                 break;
917 
918             case ExternalScoreEvaluator.BestCandidateType.SAVED_NETWORK:
919                 ScanResult scanResultCandidate = scoreEvaluator.getScanResultCandidate();
920                 networkCandidate = scoreEvaluator.getSavedConfig();
921                 networkCandidate.getNetworkSelectionStatus().setCandidate(scanResultCandidate);
922                 localLog(String.format("new scored candidate %s network ID:%d",
923                         toScanId(scanResultCandidate), networkCandidate.networkId));
924                 break;
925 
926             case ExternalScoreEvaluator.BestCandidateType.NONE:
927                 localLog("ExternalScoreEvaluator did not see any good candidates.");
928                 break;
929 
930             default:
931                 localLoge("Unhandled ExternalScoreEvaluator case. No candidate selected.");
932                 break;
933         }
934         return networkCandidate;
935     }
936 
937     /**
938      * Returns the available external network score or NULL if no score is available.
939      *
940      * @param scanResult The scan result of the network to score.
941      * @param isActiveNetwork Whether or not the network is currently connected.
942      * @return A valid external score if one is available or NULL.
943      */
944     @Nullable
getNetworkScore(ScanResult scanResult, boolean isActiveNetwork)945     Integer getNetworkScore(ScanResult scanResult, boolean isActiveNetwork) {
946         if (mNetworkScoreCache != null && mNetworkScoreCache.isScoredNetwork(scanResult)) {
947             int networkScore = mNetworkScoreCache.getNetworkScore(scanResult, isActiveNetwork);
948             localLog(toScanId(scanResult) + " has score: " + networkScore);
949             return networkScore;
950         }
951         return null;
952     }
953 
954     /**
955      * Formats the given ScanResult as a scan ID for logging.
956      */
toScanId(@ullable ScanResult scanResult)957     private static String toScanId(@Nullable ScanResult scanResult) {
958         return scanResult == null ? "NULL"
959                                   : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
960     }
961 
962     //Dump the logs
dump(FileDescriptor fd, PrintWriter pw, String[] args)963     void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
964         pw.println("Dump of WifiQualifiedNetworkSelector");
965         pw.println("WifiQualifiedNetworkSelector - Log Begin ----");
966         mLocalLog.dump(fd, pw, args);
967         pw.println("WifiQualifiedNetworkSelector - Log End ----");
968     }
969 
970     /**
971      * Used to track and evaluate networks that are assigned external scores.
972      */
973     static class ExternalScoreEvaluator {
974         @Retention(RetentionPolicy.SOURCE)
975         @interface BestCandidateType {
976             int NONE = 0;
977             int SAVED_NETWORK = 1;
978             int UNTRUSTED_NETWORK = 2;
979         }
980         // Always set to the best known candidate.
981         private @BestCandidateType int mBestCandidateType = BestCandidateType.NONE;
982         private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
983         private WifiConfiguration mSavedConfig;
984         private ScanResult mScanResultCandidate;
985         private final LocalLog mLocalLog;
986         private final boolean mDbg;
987 
ExternalScoreEvaluator(LocalLog localLog, boolean dbg)988         ExternalScoreEvaluator(LocalLog localLog, boolean dbg) {
989             mLocalLog = localLog;
990             mDbg = dbg;
991         }
992 
993         // Determines whether or not the given scan result is the best one its seen so far.
evalUntrustedCandidate(@ullable Integer score, ScanResult scanResult)994         void evalUntrustedCandidate(@Nullable Integer score, ScanResult scanResult) {
995             if (score != null && score > mHighScore) {
996                 mHighScore = score;
997                 mScanResultCandidate = scanResult;
998                 mBestCandidateType = BestCandidateType.UNTRUSTED_NETWORK;
999                 localLog(toScanId(scanResult) + " become the new untrusted candidate");
1000             }
1001         }
1002 
1003         // Determines whether or not the given saved network is the best one its seen so far.
evalSavedCandidate(@ullable Integer score, WifiConfiguration config, ScanResult scanResult)1004         void evalSavedCandidate(@Nullable Integer score, WifiConfiguration config,
1005                 ScanResult scanResult) {
1006             // Always take the highest score. If there's a tie and an untrusted network is currently
1007             // the best then pick the saved network.
1008             if (score != null
1009                     && (score > mHighScore
1010                         || (mBestCandidateType == BestCandidateType.UNTRUSTED_NETWORK
1011                             && score == mHighScore))) {
1012                 mHighScore = score;
1013                 mSavedConfig = config;
1014                 mScanResultCandidate = scanResult;
1015                 mBestCandidateType = BestCandidateType.SAVED_NETWORK;
1016                 localLog(toScanId(scanResult) + " become the new externally scored saved network "
1017                         + "candidate");
1018             }
1019         }
1020 
getBestCandidateType()1021         int getBestCandidateType() {
1022             return mBestCandidateType;
1023         }
1024 
getHighScore()1025         int getHighScore() {
1026             return mHighScore;
1027         }
1028 
getScanResultCandidate()1029         public ScanResult getScanResultCandidate() {
1030             return mScanResultCandidate;
1031         }
1032 
getSavedConfig()1033         WifiConfiguration getSavedConfig() {
1034             return mSavedConfig;
1035         }
1036 
localLog(String log)1037         private void localLog(String log) {
1038             if (mDbg) {
1039                 mLocalLog.log(log);
1040             }
1041         }
1042     }
1043 }
1044