| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wifi; |
| |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.net.NetworkKey; |
| import android.net.NetworkScoreManager; |
| import android.net.WifiKey; |
| import android.net.wifi.ScanResult; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.WifiManager; |
| import android.text.TextUtils; |
| import android.util.LocalLog; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * This class looks at all the connectivity scan results then |
| * select an network for the phone to connect/roam to. |
| */ |
| public class WifiQualifiedNetworkSelector { |
| private WifiConfigManager mWifiConfigManager; |
| private WifiInfo mWifiInfo; |
| private NetworkScoreManager mScoreManager; |
| private WifiNetworkScoreCache mNetworkScoreCache; |
| private Clock mClock; |
| private static final String TAG = "WifiQualifiedNetworkSelector:"; |
| // Always enable debugging logs for now since QNS is still a new feature. |
| private static final boolean FORCE_DEBUG = true; |
| private boolean mDbg = FORCE_DEBUG; |
| private WifiConfiguration mCurrentConnectedNetwork = null; |
| private String mCurrentBssid = null; |
| //buffer most recent scan results |
| private List<ScanDetail> mScanDetails = null; |
| //buffer of filtered scan results (Scan results considered by network selection) & associated |
| //WifiConfiguration (if any) |
| private volatile List<Pair<ScanDetail, WifiConfiguration>> mFilteredScanDetails = null; |
| |
| //Minimum time gap between last successful Qualified Network Selection and new selection attempt |
| //usable only when current state is connected state default 10 s |
| private static final int MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL = 10 * 1000; |
| |
| //if current network is on 2.4GHz band and has a RSSI over this, need not new network selection |
| public static final int QUALIFIED_RSSI_24G_BAND = -73; |
| //if current network is on 5GHz band and has a RSSI over this, need not new network selection |
| public static final int QUALIFIED_RSSI_5G_BAND = -70; |
| //any RSSI larger than this will benefit the traffic very limited |
| public static final int RSSI_SATURATION_2G_BAND = -60; |
| public static final int RSSI_SATURATION_5G_BAND = -57; |
| //Any value below this will be considered not usable |
| public static final int MINIMUM_2G_ACCEPT_RSSI = -85; |
| public static final int MINIMUM_5G_ACCEPT_RSSI = -82; |
| |
| public static final int RSSI_SCORE_SLOPE = 4; |
| public static final int RSSI_SCORE_OFFSET = 85; |
| |
| public static final int BAND_AWARD_5GHz = 40; |
| public static final int SAME_NETWORK_AWARD = 16; |
| |
| public static final int SAME_BSSID_AWARD = 24; |
| public static final int LAST_SELECTION_AWARD = 480; |
| public static final int PASSPOINT_SECURITY_AWARD = 40; |
| public static final int SECURITY_AWARD = 80; |
| public static final int BSSID_BLACKLIST_THRESHOLD = 3; |
| public static final int BSSID_BLACKLIST_EXPIRE_TIME = 5 * 60 * 1000; |
| private final int mNoIntnetPenalty; |
| //TODO: check whether we still need this one when we update the scan manager |
| public static final int SCAN_RESULT_MAXIMUNM_AGE = 40000; |
| private static final int INVALID_TIME_STAMP = -1; |
| private long mLastQualifiedNetworkSelectionTimeStamp = INVALID_TIME_STAMP; |
| |
| // Temporarily, for dog food |
| private final LocalLog mLocalLog = new LocalLog(1024); |
| private int mRssiScoreSlope = RSSI_SCORE_SLOPE; |
| private int mRssiScoreOffset = RSSI_SCORE_OFFSET; |
| private int mSameBssidAward = SAME_BSSID_AWARD; |
| private int mLastSelectionAward = LAST_SELECTION_AWARD; |
| private int mPasspointSecurityAward = PASSPOINT_SECURITY_AWARD; |
| private int mSecurityAward = SECURITY_AWARD; |
| private int mUserPreferedBand = WifiManager.WIFI_FREQUENCY_BAND_AUTO; |
| private Map<String, BssidBlacklistStatus> mBssidBlacklist = |
| new HashMap<String, BssidBlacklistStatus>(); |
| |
| /** |
| * class save the blacklist status of a given BSSID |
| */ |
| private static class BssidBlacklistStatus { |
| //how many times it is requested to be blacklisted (association rejection trigger this) |
| int mCounter; |
| boolean mIsBlacklisted; |
| long mBlacklistedTimeStamp = INVALID_TIME_STAMP; |
| } |
| |
| private void localLog(String log) { |
| if (mDbg) { |
| mLocalLog.log(log); |
| } |
| } |
| |
| private void localLoge(String log) { |
| mLocalLog.log(log); |
| } |
| |
| @VisibleForTesting |
| void setWifiNetworkScoreCache(WifiNetworkScoreCache cache) { |
| mNetworkScoreCache = cache; |
| } |
| |
| /** |
| * @return current target connected network |
| */ |
| public WifiConfiguration getConnetionTargetNetwork() { |
| return mCurrentConnectedNetwork; |
| } |
| |
| /** |
| * @return the list of ScanDetails scored as potential candidates by the last run of |
| * selectQualifiedNetwork, this will be empty if QNS determined no selection was needed on last |
| * run. This includes scan details of sufficient signal strength, and had an associated |
| * WifiConfiguration. |
| */ |
| public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() { |
| return mFilteredScanDetails; |
| } |
| |
| /** |
| * set the user selected preferred band |
| * |
| * @param band preferred band user selected |
| */ |
| public void setUserPreferredBand(int band) { |
| mUserPreferedBand = band; |
| } |
| |
| WifiQualifiedNetworkSelector(WifiConfigManager configureStore, Context context, |
| WifiInfo wifiInfo, Clock clock) { |
| mWifiConfigManager = configureStore; |
| mWifiInfo = wifiInfo; |
| mClock = clock; |
| mScoreManager = |
| (NetworkScoreManager) context.getSystemService(Context.NETWORK_SCORE_SERVICE); |
| if (mScoreManager != null) { |
| mNetworkScoreCache = new WifiNetworkScoreCache(context); |
| mScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache); |
| } else { |
| localLoge("No network score service: Couldn't register as a WiFi score Manager, type=" |
| + NetworkKey.TYPE_WIFI + " service= " + Context.NETWORK_SCORE_SERVICE); |
| mNetworkScoreCache = null; |
| } |
| |
| mRssiScoreSlope = context.getResources().getInteger( |
| R.integer.config_wifi_framework_RSSI_SCORE_SLOPE); |
| mRssiScoreOffset = context.getResources().getInteger( |
| R.integer.config_wifi_framework_RSSI_SCORE_OFFSET); |
| mSameBssidAward = context.getResources().getInteger( |
| R.integer.config_wifi_framework_SAME_BSSID_AWARD); |
| mLastSelectionAward = context.getResources().getInteger( |
| R.integer.config_wifi_framework_LAST_SELECTION_AWARD); |
| mPasspointSecurityAward = context.getResources().getInteger( |
| R.integer.config_wifi_framework_PASSPOINT_SECURITY_AWARD); |
| mSecurityAward = context.getResources().getInteger( |
| R.integer.config_wifi_framework_SECURITY_AWARD); |
| mNoIntnetPenalty = (mWifiConfigManager.mThresholdSaturatedRssi24.get() + mRssiScoreOffset) |
| * mRssiScoreSlope + mWifiConfigManager.mBandAward5Ghz.get() |
| + mWifiConfigManager.mCurrentNetworkBoost.get() + mSameBssidAward + mSecurityAward; |
| } |
| |
| void enableVerboseLogging(int verbose) { |
| mDbg = verbose > 0 || FORCE_DEBUG; |
| } |
| |
| private String getNetworkString(WifiConfiguration network) { |
| if (network == null) { |
| return null; |
| } |
| |
| return (network.SSID + ":" + network.networkId); |
| |
| } |
| |
| /** |
| * check whether current network is good enough we need not consider any potential switch |
| * |
| * @param currentNetwork -- current connected network |
| * @return true -- qualified and do not consider potential network switch |
| * false -- not good enough and should try potential network switch |
| */ |
| private boolean isNetworkQualified(WifiConfiguration currentNetwork) { |
| |
| if (currentNetwork == null) { |
| localLog("Disconnected"); |
| return false; |
| } else { |
| localLog("Current network is: " + currentNetwork.SSID + " ,ID is: " |
| + currentNetwork.networkId); |
| } |
| |
| //if current connected network is an ephemeral network,we will consider |
| // there is no current network |
| if (currentNetwork.ephemeral) { |
| localLog("Current is ephemeral. Start reselect"); |
| return false; |
| } |
| |
| //if current network is open network, not qualified |
| if (mWifiConfigManager.isOpenNetwork(currentNetwork)) { |
| localLog("Current network is open network"); |
| return false; |
| } |
| |
| // Current network band must match with user preference selection |
| if (mWifiInfo.is24GHz() && (mUserPreferedBand != WifiManager.WIFI_FREQUENCY_BAND_2GHZ)) { |
| localLog("Current band dose not match user preference. Start Qualified Network" |
| + " Selection Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band" |
| : "5GHz band") + "UserPreference band = " + mUserPreferedBand); |
| return false; |
| } |
| |
| int currentRssi = mWifiInfo.getRssi(); |
| if ((mWifiInfo.is24GHz() |
| && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi24.get()) |
| || (mWifiInfo.is5GHz() |
| && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi5.get())) { |
| localLog("Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band" : "5GHz band") |
| + "current RSSI is: " + currentRssi); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * check whether QualifiedNetworkSelection is needed or not |
| * |
| * @param isLinkDebouncing true -- Link layer is under debouncing |
| * false -- Link layer is not under debouncing |
| * @param isConnected true -- device is connected to an AP currently |
| * false -- device is not connected to an AP currently |
| * @param isDisconnected true -- WifiStateMachine is at disconnected state |
| * false -- WifiStateMachine is not at disconnected state |
| * @param isSupplicantTransientState true -- supplicant is in a transient state now |
| * false -- supplicant is not in a transient state now |
| * @return true -- need a Qualified Network Selection procedure |
| * false -- do not need a QualifiedNetworkSelection procedure |
| */ |
| private boolean needQualifiedNetworkSelection(boolean isLinkDebouncing, boolean isConnected, |
| boolean isDisconnected, boolean isSupplicantTransientState) { |
| if (mScanDetails.size() == 0) { |
| localLog("empty scan result"); |
| return false; |
| } |
| |
| // Do not trigger Qualified Network Selection during L2 link debouncing procedure |
| if (isLinkDebouncing) { |
| localLog("Need not Qualified Network Selection during L2 debouncing"); |
| return false; |
| } |
| |
| if (isConnected) { |
| //already connected. Just try to find better candidate |
| //if switch network is not allowed in connected mode, do not trigger Qualified Network |
| //Selection |
| if (!mWifiConfigManager.getEnableAutoJoinWhenAssociated()) { |
| localLog("Switch network under connection is not allowed"); |
| return false; |
| } |
| |
| //Do not select again if last selection is within |
| //MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL |
| if (mLastQualifiedNetworkSelectionTimeStamp != INVALID_TIME_STAMP) { |
| long gap = mClock.elapsedRealtime() - mLastQualifiedNetworkSelectionTimeStamp; |
| if (gap < MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL) { |
| localLog("Too short to last successful Qualified Network Selection Gap is:" |
| + gap + " ms!"); |
| return false; |
| } |
| } |
| |
| WifiConfiguration currentNetwork = |
| mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId()); |
| if (currentNetwork == null) { |
| // WifiStateMachine in connected state but WifiInfo is not. It means there is a race |
| // condition happened. Do not make QNS until WifiStateMachine goes into |
| // disconnected state |
| return false; |
| } |
| |
| if (!isNetworkQualified(mCurrentConnectedNetwork)) { |
| //need not trigger Qualified Network Selection if current network is qualified |
| localLog("Current network is not qualified"); |
| return true; |
| } else { |
| return false; |
| } |
| } else if (isDisconnected) { |
| mCurrentConnectedNetwork = null; |
| mCurrentBssid = null; |
| //Do not start Qualified Network Selection if current state is a transient state |
| if (isSupplicantTransientState) { |
| return false; |
| } |
| } else { |
| //Do not allow new network selection in other state |
| localLog("WifiStateMachine is not on connected or disconnected state"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| int calculateBssidScore(ScanResult scanResult, WifiConfiguration network, |
| WifiConfiguration currentNetwork, boolean sameBssid, boolean sameSelect, |
| StringBuffer sbuf) { |
| |
| int score = 0; |
| //calculate the RSSI score |
| int rssi = scanResult.level <= mWifiConfigManager.mThresholdSaturatedRssi24.get() |
| ? scanResult.level : mWifiConfigManager.mThresholdSaturatedRssi24.get(); |
| score += (rssi + mRssiScoreOffset) * mRssiScoreSlope; |
| sbuf.append(" RSSI score: " + score); |
| if (scanResult.is5GHz()) { |
| //5GHz band |
| score += mWifiConfigManager.mBandAward5Ghz.get(); |
| sbuf.append(" 5GHz bonus: " + mWifiConfigManager.mBandAward5Ghz.get()); |
| } |
| |
| //last user selection award |
| if (sameSelect) { |
| long timeDifference = mClock.elapsedRealtime() |
| - mWifiConfigManager.getLastSelectedTimeStamp(); |
| |
| if (timeDifference > 0) { |
| int bonus = mLastSelectionAward - (int) (timeDifference / 1000 / 60); |
| score += bonus > 0 ? bonus : 0; |
| sbuf.append(" User selected it last time " + (timeDifference / 1000 / 60) |
| + " minutes ago, bonus:" + bonus); |
| } |
| } |
| |
| //same network award |
| if (network == currentNetwork || network.isLinked(currentNetwork)) { |
| score += mWifiConfigManager.mCurrentNetworkBoost.get(); |
| sbuf.append(" Same network with current associated. Bonus: " |
| + mWifiConfigManager.mCurrentNetworkBoost.get()); |
| } |
| |
| //same BSSID award |
| if (sameBssid) { |
| score += mSameBssidAward; |
| sbuf.append(" Same BSSID with current association. Bonus: " + mSameBssidAward); |
| } |
| |
| //security award |
| if (network.isPasspoint()) { |
| score += mPasspointSecurityAward; |
| sbuf.append(" Passpoint Bonus:" + mPasspointSecurityAward); |
| } else if (!mWifiConfigManager.isOpenNetwork(network)) { |
| score += mSecurityAward; |
| sbuf.append(" Secure network Bonus:" + mSecurityAward); |
| } |
| |
| //Penalty for no internet network. Make sure if there is any network with Internet, |
| //however, if there is no any other network with internet, this network can be chosen |
| if (network.numNoInternetAccessReports > 0 && !network.validatedInternetAccess) { |
| score -= mNoIntnetPenalty; |
| sbuf.append(" No internet Penalty:-" + mNoIntnetPenalty); |
| } |
| |
| |
| sbuf.append(" Score for scanResult: " + scanResult + " and Network ID: " |
| + network.networkId + " final score:" + score + "\n\n"); |
| |
| return score; |
| } |
| |
| /** |
| * This API try to update all the saved networks' network selection status |
| */ |
| private void updateSavedNetworkSelectionStatus() { |
| List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(); |
| if (savedNetworks.size() == 0) { |
| localLog("no saved network"); |
| return; |
| } |
| |
| StringBuffer sbuf = new StringBuffer("Saved Network List\n"); |
| for (WifiConfiguration network : savedNetworks) { |
| WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId); |
| WifiConfiguration.NetworkSelectionStatus status = |
| config.getNetworkSelectionStatus(); |
| |
| //If the configuration is temporarily disabled, try to re-enable it |
| if (status.isNetworkTemporaryDisabled()) { |
| mWifiConfigManager.tryEnableQualifiedNetwork(network.networkId); |
| } |
| |
| //clean the cached candidate, score and seen |
| status.setCandidate(null); |
| status.setCandidateScore(Integer.MIN_VALUE); |
| status.setSeenInLastQualifiedNetworkSelection(false); |
| |
| //print the debug messages |
| sbuf.append(" " + getNetworkString(network) + " " + " User Preferred BSSID:" |
| + network.BSSID + " FQDN:" + network.FQDN + " " |
| + status.getNetworkStatusString() + " Disable account: "); |
| for (int index = status.NETWORK_SELECTION_ENABLE; |
| index < status.NETWORK_SELECTION_DISABLED_MAX; index++) { |
| sbuf.append(status.getDisableReasonCounter(index) + " "); |
| } |
| sbuf.append("Connect Choice:" + status.getConnectChoice() + " set time:" |
| + status.getConnectChoiceTimestamp()); |
| sbuf.append("\n"); |
| } |
| localLog(sbuf.toString()); |
| } |
| |
| /** |
| * This API is called when user explicitly select a network. Currently, it is used in following |
| * cases: |
| * (1) User explicitly choose to connect to a saved network |
| * (2) User save a network after add a new network |
| * (3) User save a network after modify a saved network |
| * Following actions will be triggered: |
| * 1. if this network is disabled, we need re-enable it again |
| * 2. we considered user prefer this network over all the networks visible in latest network |
| * selection procedure |
| * |
| * @param netId new network ID for either the network the user choose or add |
| * @param persist whether user has the authority to overwrite current connect choice |
| * @return true -- There is change made to connection choice of any saved network |
| * false -- There is no change made to connection choice of any saved network |
| */ |
| public boolean userSelectNetwork(int netId, boolean persist) { |
| WifiConfiguration selected = mWifiConfigManager.getWifiConfiguration(netId); |
| localLog("userSelectNetwork:" + netId + " persist:" + persist); |
| if (selected == null || selected.SSID == null) { |
| localLoge("userSelectNetwork: Bad configuration with nid=" + netId); |
| return false; |
| } |
| |
| |
| if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { |
| mWifiConfigManager.updateNetworkSelectionStatus(netId, |
| WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE); |
| } |
| |
| if (!persist) { |
| localLog("User has no privilege to overwrite the current priority"); |
| return false; |
| } |
| |
| boolean change = false; |
| String key = selected.configKey(); |
| // This is only used for setting the connect choice timestamp for debugging purposes. |
| long currentTime = mClock.currentTimeMillis(); |
| List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(); |
| |
| for (WifiConfiguration network : savedNetworks) { |
| WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId); |
| WifiConfiguration.NetworkSelectionStatus status = config.getNetworkSelectionStatus(); |
| if (config.networkId == selected.networkId) { |
| if (status.getConnectChoice() != null) { |
| localLog("Remove user selection preference of " + status.getConnectChoice() |
| + " Set Time: " + status.getConnectChoiceTimestamp() + " from " |
| + config.SSID + " : " + config.networkId); |
| status.setConnectChoice(null); |
| status.setConnectChoiceTimestamp(WifiConfiguration.NetworkSelectionStatus |
| .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP); |
| change = true; |
| } |
| continue; |
| } |
| |
| if (status.getSeenInLastQualifiedNetworkSelection() |
| && (status.getConnectChoice() == null |
| || !status.getConnectChoice().equals(key))) { |
| localLog("Add key:" + key + " Set Time: " + currentTime + " to " |
| + getNetworkString(config)); |
| status.setConnectChoice(key); |
| status.setConnectChoiceTimestamp(currentTime); |
| change = true; |
| } |
| } |
| //Write this change to file |
| if (change) { |
| mWifiConfigManager.writeKnownNetworkHistory(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * enable/disable a BSSID for Quality Network Selection |
| * When an association rejection event is obtained, Quality Network Selector will disable this |
| * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it |
| * successfully later, this bssid can be re-enabled. |
| * |
| * @param bssid the bssid to be enabled / disabled |
| * @param enable -- true enable a bssid if it has been disabled |
| * -- false disable a bssid |
| */ |
| public boolean enableBssidForQualityNetworkSelection(String bssid, boolean enable) { |
| if (enable) { |
| return (mBssidBlacklist.remove(bssid) != null); |
| } else { |
| if (bssid != null) { |
| BssidBlacklistStatus status = mBssidBlacklist.get(bssid); |
| if (status == null) { |
| //first time |
| BssidBlacklistStatus newStatus = new BssidBlacklistStatus(); |
| newStatus.mCounter++; |
| mBssidBlacklist.put(bssid, newStatus); |
| } else if (!status.mIsBlacklisted) { |
| status.mCounter++; |
| if (status.mCounter >= BSSID_BLACKLIST_THRESHOLD) { |
| status.mIsBlacklisted = true; |
| status.mBlacklistedTimeStamp = mClock.elapsedRealtime(); |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * update the buffered BSSID blacklist |
| * |
| * Go through the whole buffered BSSIDs blacklist and check when the BSSIDs is blocked. If they |
| * were blacked before BSSID_BLACKLIST_EXPIRE_TIME, re-enable it again. |
| */ |
| private void updateBssidBlacklist() { |
| Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator(); |
| while (iter.hasNext()) { |
| BssidBlacklistStatus status = iter.next(); |
| if (status != null && status.mIsBlacklisted) { |
| if (mClock.elapsedRealtime() - status.mBlacklistedTimeStamp |
| >= BSSID_BLACKLIST_EXPIRE_TIME) { |
| iter.remove(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Check whether a bssid is disabled |
| * @param bssid -- the bssid to check |
| * @return true -- bssid is disabled |
| * false -- bssid is not disabled |
| */ |
| public boolean isBssidDisabled(String bssid) { |
| BssidBlacklistStatus status = mBssidBlacklist.get(bssid); |
| return status == null ? false : status.mIsBlacklisted; |
| } |
| |
| /** |
| * ToDo: This should be called in Connectivity Manager when it gets new scan result |
| * check whether a network slection is needed. If need, check all the new scan results and |
| * select a new qualified network/BSSID to connect to |
| * |
| * @param forceSelectNetwork true -- start a qualified network selection anyway,no matter |
| * current network is already qualified or not. |
| * false -- if current network is already qualified, do not do new |
| * selection |
| * @param isUntrustedConnectionsAllowed true -- user allow to connect to untrusted network |
| * false -- user do not allow to connect to untrusted |
| * network |
| * @param scanDetails latest scan result obtained (should be connectivity scan only) |
| * @param isLinkDebouncing true -- Link layer is under debouncing |
| * false -- Link layer is not under debouncing |
| * @param isConnected true -- device is connected to an AP currently |
| * false -- device is not connected to an AP currently |
| * @param isDisconnected true -- WifiStateMachine is at disconnected state |
| * false -- WifiStateMachine is not at disconnected state |
| * @param isSupplicantTransient true -- supplicant is in a transient state |
| * false -- supplicant is not in a transient state |
| * @return the qualified network candidate found. If no available candidate, return null |
| */ |
| public WifiConfiguration selectQualifiedNetwork(boolean forceSelectNetwork , |
| boolean isUntrustedConnectionsAllowed, List<ScanDetail> scanDetails, |
| boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected, |
| boolean isSupplicantTransient) { |
| localLog("==========start qualified Network Selection=========="); |
| mScanDetails = scanDetails; |
| List<Pair<ScanDetail, WifiConfiguration>> filteredScanDetails = new ArrayList<>(); |
| if (mCurrentConnectedNetwork == null) { |
| mCurrentConnectedNetwork = |
| mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId()); |
| } |
| |
| if (mCurrentBssid == null) { |
| mCurrentBssid = mWifiInfo.getBSSID(); |
| } |
| |
| if (!forceSelectNetwork && !needQualifiedNetworkSelection(isLinkDebouncing, isConnected, |
| isDisconnected, isSupplicantTransient)) { |
| localLog("Quit qualified Network Selection since it is not forced and current network" |
| + " is qualified already"); |
| mFilteredScanDetails = filteredScanDetails; |
| return null; |
| } |
| |
| int currentHighestScore = Integer.MIN_VALUE; |
| ScanResult scanResultCandidate = null; |
| WifiConfiguration networkCandidate = null; |
| final ExternalScoreEvaluator externalScoreEvaluator = |
| new ExternalScoreEvaluator(mLocalLog, mDbg); |
| String lastUserSelectedNetWorkKey = mWifiConfigManager.getLastSelectedConfiguration(); |
| WifiConfiguration lastUserSelectedNetwork = |
| mWifiConfigManager.getWifiConfiguration(lastUserSelectedNetWorkKey); |
| if (lastUserSelectedNetwork != null) { |
| localLog("Last selection is " + lastUserSelectedNetwork.SSID + " Time to now: " |
| + ((mClock.elapsedRealtime() - mWifiConfigManager.getLastSelectedTimeStamp()) |
| / 1000 / 60 + " minutes")); |
| } |
| |
| updateSavedNetworkSelectionStatus(); |
| updateBssidBlacklist(); |
| |
| StringBuffer lowSignalScan = new StringBuffer(); |
| StringBuffer notSavedScan = new StringBuffer(); |
| StringBuffer noValidSsid = new StringBuffer(); |
| StringBuffer scoreHistory = new StringBuffer(); |
| ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>(); |
| |
| //iterate all scan results and find the best candidate with the highest score |
| for (ScanDetail scanDetail : mScanDetails) { |
| ScanResult scanResult = scanDetail.getScanResult(); |
| //skip bad scan result |
| if (scanResult.SSID == null || TextUtils.isEmpty(scanResult.SSID)) { |
| if (mDbg) { |
| //We should not see this in ePNO |
| noValidSsid.append(scanResult.BSSID + " / "); |
| } |
| continue; |
| } |
| |
| final String scanId = toScanId(scanResult); |
| //check whether this BSSID is blocked or not |
| if (mWifiConfigManager.isBssidBlacklisted(scanResult.BSSID) |
| || isBssidDisabled(scanResult.BSSID)) { |
| //We should not see this in ePNO |
| Log.e(TAG, scanId + " is in blacklist."); |
| continue; |
| } |
| |
| //skip scan result with too weak signals |
| if ((scanResult.is24GHz() && scanResult.level |
| < mWifiConfigManager.mThresholdMinimumRssi24.get()) |
| || (scanResult.is5GHz() && scanResult.level |
| < mWifiConfigManager.mThresholdMinimumRssi5.get())) { |
| if (mDbg) { |
| lowSignalScan.append(scanId + "(" + (scanResult.is24GHz() ? "2.4GHz" : "5GHz") |
| + ")" + scanResult.level + " / "); |
| } |
| continue; |
| } |
| |
| //check if there is already a score for this network |
| if (mNetworkScoreCache != null && !mNetworkScoreCache.isScoredNetwork(scanResult)) { |
| //no score for this network yet. |
| WifiKey wifiKey; |
| |
| try { |
| wifiKey = new WifiKey("\"" + scanResult.SSID + "\"", scanResult.BSSID); |
| NetworkKey ntwkKey = new NetworkKey(wifiKey); |
| //add to the unscoredNetworks list so we can request score later |
| unscoredNetworks.add(ntwkKey); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "Invalid SSID=" + scanResult.SSID + " BSSID=" + scanResult.BSSID |
| + " for network score. Skip."); |
| } |
| } |
| |
| //check whether this scan result belong to a saved network |
| boolean potentiallyEphemeral = false; |
| // Stores WifiConfiguration of potential connection candidates for scan result filtering |
| WifiConfiguration potentialEphemeralCandidate = null; |
| List<WifiConfiguration> associatedWifiConfigurations = |
| mWifiConfigManager.updateSavedNetworkWithNewScanDetail(scanDetail, |
| isSupplicantTransient || isConnected || isLinkDebouncing); |
| if (associatedWifiConfigurations == null) { |
| potentiallyEphemeral = true; |
| if (mDbg) { |
| notSavedScan.append(scanId + " / "); |
| } |
| } else if (associatedWifiConfigurations.size() == 1) { |
| //if there are more than 1 associated network, it must be a passpoint network |
| WifiConfiguration network = associatedWifiConfigurations.get(0); |
| if (network.ephemeral) { |
| potentialEphemeralCandidate = network; |
| potentiallyEphemeral = true; |
| } |
| } |
| |
| // Evaluate the potentially ephemeral network as a possible candidate if untrusted |
| // connections are allowed and we have an external score for the scan result. |
| if (potentiallyEphemeral) { |
| if (isUntrustedConnectionsAllowed) { |
| Integer netScore = getNetworkScore(scanResult, false); |
| if (netScore != null |
| && !mWifiConfigManager.wasEphemeralNetworkDeleted(scanResult.SSID)) { |
| externalScoreEvaluator.evalUntrustedCandidate(netScore, scanResult); |
| // scanDetail is for available ephemeral network |
| filteredScanDetails.add(Pair.create(scanDetail, |
| potentialEphemeralCandidate)); |
| } |
| } |
| continue; |
| } |
| |
| // calculate the score of each scanresult whose associated network is not ephemeral. Due |
| // to one scan result can associated with more than 1 network, we need calculate all |
| // the scores and use the highest one as the scanresults score. |
| int highestScore = Integer.MIN_VALUE; |
| int score; |
| WifiConfiguration configurationCandidateForThisScan = null; |
| WifiConfiguration potentialCandidate = null; |
| for (WifiConfiguration network : associatedWifiConfigurations) { |
| WifiConfiguration.NetworkSelectionStatus status = |
| network.getNetworkSelectionStatus(); |
| status.setSeenInLastQualifiedNetworkSelection(true); |
| if (potentialCandidate == null) { |
| potentialCandidate = network; |
| } |
| if (!status.isNetworkEnabled()) { |
| continue; |
| } else if (network.BSSID != null && !network.BSSID.equals("any") |
| && !network.BSSID.equals(scanResult.BSSID)) { |
| //in such scenario, user (APP) has specified the only BSSID to connect for this |
| // configuration. So only the matched scan result can be candidate |
| localLog("Network: " + getNetworkString(network) + " has specified" + "BSSID:" |
| + network.BSSID + ". Skip " + scanResult.BSSID); |
| continue; |
| } |
| |
| // If the network is marked to use external scores then attempt to fetch the score. |
| // These networks will not be considered alongside the other saved networks. |
| if (network.useExternalScores) { |
| Integer netScore = getNetworkScore(scanResult, false); |
| externalScoreEvaluator.evalSavedCandidate(netScore, network, scanResult); |
| continue; |
| } |
| |
| score = calculateBssidScore(scanResult, network, mCurrentConnectedNetwork, |
| (mCurrentBssid == null ? false : mCurrentBssid.equals(scanResult.BSSID)), |
| (lastUserSelectedNetwork == null ? false : lastUserSelectedNetwork.networkId |
| == network.networkId), scoreHistory); |
| if (score > highestScore) { |
| highestScore = score; |
| configurationCandidateForThisScan = network; |
| potentialCandidate = network; |
| } |
| //update the cached candidate |
| if (score > status.getCandidateScore()) { |
| status.setCandidate(scanResult); |
| status.setCandidateScore(score); |
| } |
| } |
| // Create potential filteredScanDetail entry |
| filteredScanDetails.add(Pair.create(scanDetail, potentialCandidate)); |
| |
| if (highestScore > currentHighestScore || (highestScore == currentHighestScore |
| && scanResultCandidate != null |
| && scanResult.level > scanResultCandidate.level)) { |
| currentHighestScore = highestScore; |
| scanResultCandidate = scanResult; |
| networkCandidate = configurationCandidateForThisScan; |
| } |
| } |
| |
| mFilteredScanDetails = filteredScanDetails; |
| |
| //kick the score manager if there is any unscored network |
| if (mScoreManager != null && unscoredNetworks.size() != 0) { |
| NetworkKey[] unscoredNetworkKeys = |
| unscoredNetworks.toArray(new NetworkKey[unscoredNetworks.size()]); |
| mScoreManager.requestScores(unscoredNetworkKeys); |
| } |
| |
| if (mDbg) { |
| localLog(lowSignalScan + " skipped due to low signal\n"); |
| localLog(notSavedScan + " skipped due to not saved\n "); |
| localLog(noValidSsid + " skipped due to not valid SSID\n"); |
| localLog(scoreHistory.toString()); |
| } |
| |
| //we need traverse the whole user preference to choose the one user like most now |
| if (scanResultCandidate != null) { |
| WifiConfiguration tempConfig = networkCandidate; |
| |
| while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) { |
| String key = tempConfig.getNetworkSelectionStatus().getConnectChoice(); |
| tempConfig = mWifiConfigManager.getWifiConfiguration(key); |
| |
| if (tempConfig != null) { |
| WifiConfiguration.NetworkSelectionStatus tempStatus = |
| tempConfig.getNetworkSelectionStatus(); |
| if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) { |
| scanResultCandidate = tempStatus.getCandidate(); |
| networkCandidate = tempConfig; |
| } |
| } else { |
| //we should not come here in theory |
| localLoge("Connect choice: " + key + " has no corresponding saved config"); |
| break; |
| } |
| } |
| localLog("After user choice adjust, the final candidate is:" |
| + getNetworkString(networkCandidate) + " : " + scanResultCandidate.BSSID); |
| } |
| |
| // At this point none of the saved networks were good candidates so we fall back to |
| // externally scored networks if any are available. |
| if (scanResultCandidate == null) { |
| localLog("Checking the externalScoreEvaluator for candidates..."); |
| networkCandidate = getExternalScoreCandidate(externalScoreEvaluator); |
| if (networkCandidate != null) { |
| scanResultCandidate = networkCandidate.getNetworkSelectionStatus().getCandidate(); |
| } |
| } |
| |
| if (scanResultCandidate == null) { |
| localLog("Can not find any suitable candidates"); |
| return null; |
| } |
| |
| String currentAssociationId = mCurrentConnectedNetwork == null ? "Disconnected" : |
| getNetworkString(mCurrentConnectedNetwork); |
| String targetAssociationId = getNetworkString(networkCandidate); |
| //In passpoint, saved configuration has garbage SSID. We need update it with the SSID of |
| //the scan result. |
| if (networkCandidate.isPasspoint()) { |
| // This will update the passpoint configuration in WifiConfigManager |
| networkCandidate.SSID = "\"" + scanResultCandidate.SSID + "\""; |
| } |
| |
| //For debug purpose only |
| if (scanResultCandidate.BSSID.equals(mCurrentBssid)) { |
| localLog(currentAssociationId + " is already the best choice!"); |
| } else if (mCurrentConnectedNetwork != null |
| && (mCurrentConnectedNetwork.networkId == networkCandidate.networkId |
| || mCurrentConnectedNetwork.isLinked(networkCandidate))) { |
| localLog("Roaming from " + currentAssociationId + " to " + targetAssociationId); |
| } else { |
| localLog("reconnect from " + currentAssociationId + " to " + targetAssociationId); |
| } |
| |
| mCurrentBssid = scanResultCandidate.BSSID; |
| mCurrentConnectedNetwork = networkCandidate; |
| mLastQualifiedNetworkSelectionTimeStamp = mClock.elapsedRealtime(); |
| return networkCandidate; |
| } |
| |
| /** |
| * Returns the best candidate network according to the given ExternalScoreEvaluator. |
| */ |
| @Nullable |
| WifiConfiguration getExternalScoreCandidate(ExternalScoreEvaluator scoreEvaluator) { |
| WifiConfiguration networkCandidate = null; |
| switch (scoreEvaluator.getBestCandidateType()) { |
| case ExternalScoreEvaluator.BestCandidateType.UNTRUSTED_NETWORK: |
| ScanResult untrustedScanResultCandidate = |
| scoreEvaluator.getScanResultCandidate(); |
| WifiConfiguration unTrustedNetworkCandidate = |
| mWifiConfigManager.wifiConfigurationFromScanResult( |
| untrustedScanResultCandidate); |
| |
| // Mark this config as ephemeral so it isn't persisted. |
| unTrustedNetworkCandidate.ephemeral = true; |
| if (mNetworkScoreCache != null) { |
| unTrustedNetworkCandidate.meteredHint = |
| mNetworkScoreCache.getMeteredHint(untrustedScanResultCandidate); |
| } |
| mWifiConfigManager.saveNetwork(unTrustedNetworkCandidate, |
| WifiConfiguration.UNKNOWN_UID); |
| |
| localLog(String.format("new ephemeral candidate %s network ID:%d, " |
| + "meteredHint=%b", |
| toScanId(untrustedScanResultCandidate), unTrustedNetworkCandidate.networkId, |
| unTrustedNetworkCandidate.meteredHint)); |
| |
| unTrustedNetworkCandidate.getNetworkSelectionStatus() |
| .setCandidate(untrustedScanResultCandidate); |
| networkCandidate = unTrustedNetworkCandidate; |
| break; |
| |
| case ExternalScoreEvaluator.BestCandidateType.SAVED_NETWORK: |
| ScanResult scanResultCandidate = scoreEvaluator.getScanResultCandidate(); |
| networkCandidate = scoreEvaluator.getSavedConfig(); |
| networkCandidate.getNetworkSelectionStatus().setCandidate(scanResultCandidate); |
| localLog(String.format("new scored candidate %s network ID:%d", |
| toScanId(scanResultCandidate), networkCandidate.networkId)); |
| break; |
| |
| case ExternalScoreEvaluator.BestCandidateType.NONE: |
| localLog("ExternalScoreEvaluator did not see any good candidates."); |
| break; |
| |
| default: |
| localLoge("Unhandled ExternalScoreEvaluator case. No candidate selected."); |
| break; |
| } |
| return networkCandidate; |
| } |
| |
| /** |
| * Returns the available external network score or NULL if no score is available. |
| * |
| * @param scanResult The scan result of the network to score. |
| * @param isActiveNetwork Whether or not the network is currently connected. |
| * @return A valid external score if one is available or NULL. |
| */ |
| @Nullable |
| Integer getNetworkScore(ScanResult scanResult, boolean isActiveNetwork) { |
| if (mNetworkScoreCache != null && mNetworkScoreCache.isScoredNetwork(scanResult)) { |
| int networkScore = mNetworkScoreCache.getNetworkScore(scanResult, isActiveNetwork); |
| localLog(toScanId(scanResult) + " has score: " + networkScore); |
| return networkScore; |
| } |
| return null; |
| } |
| |
| /** |
| * Formats the given ScanResult as a scan ID for logging. |
| */ |
| private static String toScanId(@Nullable ScanResult scanResult) { |
| return scanResult == null ? "NULL" |
| : String.format("%s:%s", scanResult.SSID, scanResult.BSSID); |
| } |
| |
| //Dump the logs |
| void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("Dump of WifiQualifiedNetworkSelector"); |
| pw.println("WifiQualifiedNetworkSelector - Log Begin ----"); |
| mLocalLog.dump(fd, pw, args); |
| pw.println("WifiQualifiedNetworkSelector - Log End ----"); |
| } |
| |
| /** |
| * Used to track and evaluate networks that are assigned external scores. |
| */ |
| static class ExternalScoreEvaluator { |
| @Retention(RetentionPolicy.SOURCE) |
| @interface BestCandidateType { |
| int NONE = 0; |
| int SAVED_NETWORK = 1; |
| int UNTRUSTED_NETWORK = 2; |
| } |
| // Always set to the best known candidate. |
| private @BestCandidateType int mBestCandidateType = BestCandidateType.NONE; |
| private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE; |
| private WifiConfiguration mSavedConfig; |
| private ScanResult mScanResultCandidate; |
| private final LocalLog mLocalLog; |
| private final boolean mDbg; |
| |
| ExternalScoreEvaluator(LocalLog localLog, boolean dbg) { |
| mLocalLog = localLog; |
| mDbg = dbg; |
| } |
| |
| // Determines whether or not the given scan result is the best one its seen so far. |
| void evalUntrustedCandidate(@Nullable Integer score, ScanResult scanResult) { |
| if (score != null && score > mHighScore) { |
| mHighScore = score; |
| mScanResultCandidate = scanResult; |
| mBestCandidateType = BestCandidateType.UNTRUSTED_NETWORK; |
| localLog(toScanId(scanResult) + " become the new untrusted candidate"); |
| } |
| } |
| |
| // Determines whether or not the given saved network is the best one its seen so far. |
| void evalSavedCandidate(@Nullable Integer score, WifiConfiguration config, |
| ScanResult scanResult) { |
| // Always take the highest score. If there's a tie and an untrusted network is currently |
| // the best then pick the saved network. |
| if (score != null |
| && (score > mHighScore |
| || (mBestCandidateType == BestCandidateType.UNTRUSTED_NETWORK |
| && score == mHighScore))) { |
| mHighScore = score; |
| mSavedConfig = config; |
| mScanResultCandidate = scanResult; |
| mBestCandidateType = BestCandidateType.SAVED_NETWORK; |
| localLog(toScanId(scanResult) + " become the new externally scored saved network " |
| + "candidate"); |
| } |
| } |
| |
| int getBestCandidateType() { |
| return mBestCandidateType; |
| } |
| |
| int getHighScore() { |
| return mHighScore; |
| } |
| |
| public ScanResult getScanResultCandidate() { |
| return mScanResultCandidate; |
| } |
| |
| WifiConfiguration getSavedConfig() { |
| return mSavedConfig; |
| } |
| |
| private void localLog(String log) { |
| if (mDbg) { |
| mLocalLog.log(log); |
| } |
| } |
| } |
| } |