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