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