Home | History | Annotate | Download | only in wifi
      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 
    123     private void localLog(String log) {
    124         if (mDbg) {
    125             mLocalLog.log(log);
    126         }
    127     }
    128 
    129     private void localLoge(String log) {
    130         mLocalLog.log(log);
    131     }
    132 
    133     @VisibleForTesting
    134     void setWifiNetworkScoreCache(WifiNetworkScoreCache cache) {
    135         mNetworkScoreCache = cache;
    136     }
    137 
    138     /**
    139      * @return current target connected network
    140      */
    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      */
    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      */
    160     public void setUserPreferredBand(int band) {
    161         mUserPreferedBand = band;
    162     }
    163 
    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 
    197     void enableVerboseLogging(int verbose) {
    198         mDbg = verbose > 0 || FORCE_DEBUG;
    199     }
    200 
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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
    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 
    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.
    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.
   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 
   1021         int getBestCandidateType() {
   1022             return mBestCandidateType;
   1023         }
   1024 
   1025         int getHighScore() {
   1026             return mHighScore;
   1027         }
   1028 
   1029         public ScanResult getScanResultCandidate() {
   1030             return mScanResultCandidate;
   1031         }
   1032 
   1033         WifiConfiguration getSavedConfig() {
   1034             return mSavedConfig;
   1035         }
   1036 
   1037         private void localLog(String log) {
   1038             if (mDbg) {
   1039                 mLocalLog.log(log);
   1040             }
   1041         }
   1042     }
   1043 }
   1044