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