1/* 2 * Copyright (C) 2016 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 17package com.android.server.wifi; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.content.Context; 22import android.net.NetworkKey; 23import android.net.wifi.ScanResult; 24import android.net.wifi.WifiConfiguration; 25import android.net.wifi.WifiInfo; 26import android.text.TextUtils; 27import android.util.LocalLog; 28import android.util.Pair; 29 30import com.android.internal.R; 31import com.android.internal.annotations.VisibleForTesting; 32import com.android.server.wifi.util.ScanResultUtil; 33 34import java.util.ArrayList; 35import java.util.HashSet; 36import java.util.List; 37 38/** 39 * This class looks at all the connectivity scan results then 40 * selects a network for the phone to connect or roam to. 41 */ 42public class WifiNetworkSelector { 43 private static final String TAG = "WifiNetworkSelector"; 44 45 private static final long INVALID_TIME_STAMP = Long.MIN_VALUE; 46 // Minimum time gap between last successful network selection and a new selection 47 // attempt. 48 @VisibleForTesting 49 public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000; 50 51 private final WifiConfigManager mWifiConfigManager; 52 private final Clock mClock; 53 private final LocalLog mLocalLog; 54 private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP; 55 // Buffer of filtered scan results (Scan results considered by network selection) & associated 56 // WifiConfiguration (if any). 57 private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks = 58 new ArrayList<>(); 59 private List<ScanDetail> mFilteredNetworks = new ArrayList<>(); 60 private final ScoringParams mScoringParams; 61 private final int mStayOnNetworkMinimumTxRate; 62 private final int mStayOnNetworkMinimumRxRate; 63 private final boolean mEnableAutoJoinWhenAssociated; 64 65 /** 66 * WiFi Network Selector supports various types of networks. Each type can 67 * have its evaluator to choose the best WiFi network for the device to connect 68 * to. When registering a WiFi network evaluator with the WiFi Network Selector, 69 * the priority of the network must be specified, and it must be a value between 70 * 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi 71 * Network Selector iterates through the registered scorers from the highest priority 72 * to the lowest till a network is selected. 73 */ 74 public static final int EVALUATOR_MIN_PRIORITY = 6; 75 76 /** 77 * Maximum number of evaluators can be registered with Wifi Network Selector. 78 */ 79 public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY; 80 81 /** 82 * Interface for WiFi Network Evaluator 83 * 84 * A network scorer evaulates all the networks from the scan results and 85 * recommends the best network in its category to connect or roam to. 86 */ 87 public interface NetworkEvaluator { 88 /** 89 * Get the evaluator name. 90 */ 91 String getName(); 92 93 /** 94 * Update the evaluator. 95 * 96 * Certain evaluators have to be updated with the new scan results. For example 97 * the ExternalScoreEvalutor needs to refresh its Score Cache. 98 * 99 * @param scanDetails a list of scan details constructed from the scan results 100 */ 101 void update(List<ScanDetail> scanDetails); 102 103 /** 104 * Evaluate all the networks from the scan results. 105 * 106 * @param scanDetails a list of scan details constructed from the scan results 107 * @param currentNetwork configuration of the current connected network 108 * or null if disconnected 109 * @param currentBssid BSSID of the current connected network or null if 110 * disconnected 111 * @param connected a flag to indicate if WifiStateMachine is in connected 112 * state 113 * @param untrustedNetworkAllowed a flag to indidate if untrusted networks like 114 * ephemeral networks are allowed 115 * @param connectableNetworks a list of the ScanDetail and WifiConfiguration 116 * pair which is used by the WifiLastResortWatchdog 117 * @return configuration of the chosen network; 118 * null if no network in this category is available. 119 */ 120 @Nullable 121 WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails, 122 WifiConfiguration currentNetwork, String currentBssid, 123 boolean connected, boolean untrustedNetworkAllowed, 124 List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks); 125 } 126 127 private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS]; 128 129 // A helper to log debugging information in the local log buffer, which can 130 // be retrieved in bugreport. 131 private void localLog(String log) { 132 mLocalLog.log(log); 133 } 134 135 private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails) { 136 WifiConfiguration network = 137 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 138 139 // Currently connected? 140 if (network == null) { 141 localLog("No current connected network."); 142 return false; 143 } else { 144 localLog("Current connected network: " + network.SSID 145 + " , ID: " + network.networkId); 146 } 147 148 int currentRssi = wifiInfo.getRssi(); 149 boolean hasQualifiedRssi = currentRssi 150 > mScoringParams.getSufficientRssi(wifiInfo.getFrequency()); 151 boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate) 152 || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate); 153 if (hasQualifiedRssi && hasActiveStream) { 154 localLog("Stay on current network because of good RSSI and ongoing traffic"); 155 return true; 156 } 157 158 // Ephemeral network is not qualified. 159 if (network.ephemeral) { 160 localLog("Current network is an ephemeral one."); 161 return false; 162 } 163 164 // Open network is not qualified. 165 if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) { 166 localLog("Current network is a open one."); 167 return false; 168 } 169 170 if (wifiInfo.is24GHz()) { 171 // 2.4GHz networks is not qualified whenever 5GHz is available 172 if (is5GHzNetworkAvailable(scanDetails)) { 173 localLog("Current network is 2.4GHz. 5GHz networks available."); 174 return false; 175 } 176 } 177 if (!hasQualifiedRssi) { 178 localLog("Current network RSSI[" + currentRssi + "]-acceptable but not qualified."); 179 return false; 180 } 181 182 // Network with no internet access reports is not qualified. 183 if (network.numNoInternetAccessReports > 0 && !network.noInternetAccessExpected) { 184 localLog("Current network has [" + network.numNoInternetAccessReports 185 + "] no-internet access reports."); 186 return false; 187 } 188 return true; 189 } 190 191 // Determine whether there are any 5GHz networks in the scan result 192 private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) { 193 for (ScanDetail detail : scanDetails) { 194 ScanResult result = detail.getScanResult(); 195 if (result.is5GHz()) return true; 196 } 197 return false; 198 } 199 200 private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, 201 boolean connected, boolean disconnected) { 202 if (scanDetails.size() == 0) { 203 localLog("Empty connectivity scan results. Skip network selection."); 204 return false; 205 } 206 207 if (connected) { 208 // Is roaming allowed? 209 if (!mEnableAutoJoinWhenAssociated) { 210 localLog("Switching networks in connected state is not allowed." 211 + " Skip network selection."); 212 return false; 213 } 214 215 // Has it been at least the minimum interval since last network selection? 216 if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) { 217 long gap = mClock.getElapsedSinceBootMillis() 218 - mLastNetworkSelectionTimeStamp; 219 if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) { 220 localLog("Too short since last network selection: " + gap + " ms." 221 + " Skip network selection."); 222 return false; 223 } 224 } 225 226 if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) { 227 localLog("Current connected network already sufficient. Skip network selection."); 228 return false; 229 } else { 230 localLog("Current connected network is not sufficient."); 231 return true; 232 } 233 } else if (disconnected) { 234 return true; 235 } else { 236 // No network selection if WifiStateMachine is in a state other than 237 // CONNECTED or DISCONNECTED. 238 localLog("WifiStateMachine is in neither CONNECTED nor DISCONNECTED state." 239 + " Skip network selection."); 240 return false; 241 } 242 } 243 244 /** 245 * Format the given ScanResult as a scan ID for logging. 246 */ 247 public static String toScanId(@Nullable ScanResult scanResult) { 248 return scanResult == null ? "NULL" 249 : String.format("%s:%s", scanResult.SSID, scanResult.BSSID); 250 } 251 252 /** 253 * Format the given WifiConfiguration as a SSID:netId string 254 */ 255 public static String toNetworkString(WifiConfiguration network) { 256 if (network == null) { 257 return null; 258 } 259 260 return (network.SSID + ":" + network.networkId); 261 } 262 263 /** 264 * Compares ScanResult level against the minimum threshold for its band, returns true if lower 265 */ 266 public boolean isSignalTooWeak(ScanResult scanResult) { 267 return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency)); 268 } 269 270 private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails, 271 HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) { 272 ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>(); 273 List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>(); 274 StringBuffer noValidSsid = new StringBuffer(); 275 StringBuffer blacklistedBssid = new StringBuffer(); 276 StringBuffer lowRssi = new StringBuffer(); 277 boolean scanResultsHaveCurrentBssid = false; 278 279 for (ScanDetail scanDetail : scanDetails) { 280 ScanResult scanResult = scanDetail.getScanResult(); 281 282 if (TextUtils.isEmpty(scanResult.SSID)) { 283 noValidSsid.append(scanResult.BSSID).append(" / "); 284 continue; 285 } 286 287 // Check if the scan results contain the currently connected BSSID 288 if (scanResult.BSSID.equals(currentBssid)) { 289 scanResultsHaveCurrentBssid = true; 290 } 291 292 final String scanId = toScanId(scanResult); 293 294 if (bssidBlacklist.contains(scanResult.BSSID)) { 295 blacklistedBssid.append(scanId).append(" / "); 296 continue; 297 } 298 299 // Skip network with too weak signals. 300 if (isSignalTooWeak(scanResult)) { 301 lowRssi.append(scanId).append("(") 302 .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz") 303 .append(")").append(scanResult.level).append(" / "); 304 continue; 305 } 306 307 validScanDetails.add(scanDetail); 308 } 309 310 // WNS listens to all single scan results. Some scan requests may not include 311 // the channel of the currently connected network, so the currently connected 312 // network won't show up in the scan results. We don't act on these scan results 313 // to avoid aggressive network switching which might trigger disconnection. 314 if (isConnected && !scanResultsHaveCurrentBssid) { 315 localLog("Current connected BSSID " + currentBssid + " is not in the scan results." 316 + " Skip network selection."); 317 validScanDetails.clear(); 318 return validScanDetails; 319 } 320 321 if (noValidSsid.length() != 0) { 322 localLog("Networks filtered out due to invalid SSID: " + noValidSsid); 323 } 324 325 if (blacklistedBssid.length() != 0) { 326 localLog("Networks filtered out due to blacklist: " + blacklistedBssid); 327 } 328 329 if (lowRssi.length() != 0) { 330 localLog("Networks filtered out due to low signal strength: " + lowRssi); 331 } 332 333 return validScanDetails; 334 } 335 336 /** 337 * This returns a list of ScanDetails that were filtered in the process of network selection. 338 * The list is further filtered for only open unsaved networks. 339 * 340 * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS, 341 * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are 342 * no open unsaved networks, or when network selection has not been run. 343 */ 344 public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() { 345 List<ScanDetail> openUnsavedNetworks = new ArrayList<>(); 346 for (ScanDetail scanDetail : mFilteredNetworks) { 347 ScanResult scanResult = scanDetail.getScanResult(); 348 349 if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) { 350 continue; 351 } 352 353 // Skip saved networks 354 if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) { 355 continue; 356 } 357 358 openUnsavedNetworks.add(scanDetail); 359 } 360 return openUnsavedNetworks; 361 } 362 363 /** 364 * This returns a list of ScanDetails that were filtered in the process of network selection. 365 * The list is further filtered for only carrier unsaved networks with EAP encryption. 366 * 367 * @param carrierConfig CarrierNetworkConfig used to filter carrier networks 368 * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS, 369 * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an 370 * empty list when there are no such networks, or when network selection has not been run. 371 */ 372 public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks( 373 CarrierNetworkConfig carrierConfig) { 374 List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>(); 375 for (ScanDetail scanDetail : mFilteredNetworks) { 376 ScanResult scanResult = scanDetail.getScanResult(); 377 378 if (!ScanResultUtil.isScanResultForEapNetwork(scanResult) 379 || !carrierConfig.isCarrierNetwork(scanResult.SSID)) { 380 continue; 381 } 382 383 // Skip saved networks 384 if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) { 385 continue; 386 } 387 388 carrierUnsavedNetworks.add(scanDetail); 389 } 390 return carrierUnsavedNetworks; 391 } 392 393 /** 394 * @return the list of ScanDetails scored as potential candidates by the last run of 395 * selectNetwork, this will be empty if Network selector determined no selection was 396 * needed on last run. This includes scan details of sufficient signal strength, and 397 * had an associated WifiConfiguration. 398 */ 399 public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() { 400 return mConnectableNetworks; 401 } 402 403 /** 404 * This API is called when user explicitly selects a network. Currently, it is used in following 405 * cases: 406 * (1) User explicitly chooses to connect to a saved network. 407 * (2) User saves a network after adding a new network. 408 * (3) User saves a network after modifying a saved network. 409 * Following actions will be triggered: 410 * 1. If this network is disabled, we need re-enable it again. 411 * 2. This network is favored over all the other networks visible in latest network 412 * selection procedure. 413 * 414 * @param netId ID for the network chosen by the user 415 * @return true -- There is change made to connection choice of any saved network. 416 * false -- There is no change made to connection choice of any saved network. 417 */ 418 public boolean setUserConnectChoice(int netId) { 419 localLog("userSelectNetwork: network ID=" + netId); 420 WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId); 421 422 if (selected == null || selected.SSID == null) { 423 localLog("userSelectNetwork: Invalid configuration with nid=" + netId); 424 return false; 425 } 426 427 // Enable the network if it is disabled. 428 if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { 429 mWifiConfigManager.updateNetworkSelectionStatus(netId, 430 WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE); 431 } 432 433 boolean change = false; 434 String key = selected.configKey(); 435 // This is only used for setting the connect choice timestamp for debugging purposes. 436 long currentTime = mClock.getWallClockMillis(); 437 List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(); 438 439 for (WifiConfiguration network : savedNetworks) { 440 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 441 if (network.networkId == selected.networkId) { 442 if (status.getConnectChoice() != null) { 443 localLog("Remove user selection preference of " + status.getConnectChoice() 444 + " Set Time: " + status.getConnectChoiceTimestamp() + " from " 445 + network.SSID + " : " + network.networkId); 446 mWifiConfigManager.clearNetworkConnectChoice(network.networkId); 447 change = true; 448 } 449 continue; 450 } 451 452 if (status.getSeenInLastQualifiedNetworkSelection() 453 && (status.getConnectChoice() == null 454 || !status.getConnectChoice().equals(key))) { 455 localLog("Add key: " + key + " Set Time: " + currentTime + " to " 456 + toNetworkString(network)); 457 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime); 458 change = true; 459 } 460 } 461 462 return change; 463 } 464 465 /** 466 * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen 467 * {@link WifiConfiguration} if one exists. 468 * 469 * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise 470 */ 471 private WifiConfiguration overrideCandidateWithUserConnectChoice( 472 @NonNull WifiConfiguration candidate) { 473 WifiConfiguration tempConfig = candidate; 474 WifiConfiguration originalCandidate = candidate; 475 ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate(); 476 477 while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) { 478 String key = tempConfig.getNetworkSelectionStatus().getConnectChoice(); 479 tempConfig = mWifiConfigManager.getConfiguredNetwork(key); 480 481 if (tempConfig != null) { 482 WifiConfiguration.NetworkSelectionStatus tempStatus = 483 tempConfig.getNetworkSelectionStatus(); 484 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) { 485 scanResultCandidate = tempStatus.getCandidate(); 486 candidate = tempConfig; 487 } 488 } else { 489 localLog("Connect choice: " + key + " has no corresponding saved config."); 490 break; 491 } 492 } 493 494 if (candidate != originalCandidate) { 495 localLog("After user selection adjustment, the final candidate is:" 496 + WifiNetworkSelector.toNetworkString(candidate) + " : " 497 + scanResultCandidate.BSSID); 498 } 499 return candidate; 500 } 501 502 /** 503 * Select the best network from the ones in range. 504 * 505 * @param scanDetails List of ScanDetail for all the APs in range 506 * @param bssidBlacklist Blacklisted BSSIDs 507 * @param wifiInfo Currently connected network 508 * @param connected True if the device is connected 509 * @param disconnected True if the device is disconnected 510 * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection 511 * @return Configuration of the selected network, or Null if nothing 512 */ 513 @Nullable 514 public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, 515 HashSet<String> bssidBlacklist, WifiInfo wifiInfo, 516 boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) { 517 mFilteredNetworks.clear(); 518 mConnectableNetworks.clear(); 519 if (scanDetails.size() == 0) { 520 localLog("Empty connectivity scan result"); 521 return null; 522 } 523 524 WifiConfiguration currentNetwork = 525 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 526 527 // Always get the current BSSID from WifiInfo in case that firmware initiated 528 // roaming happened. 529 String currentBssid = wifiInfo.getBSSID(); 530 531 // Shall we start network selection at all? 532 if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { 533 return null; 534 } 535 536 // Update the registered network evaluators. 537 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 538 if (registeredEvaluator != null) { 539 registeredEvaluator.update(scanDetails); 540 } 541 } 542 543 // Filter out unwanted networks. 544 mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist, 545 connected, currentBssid); 546 if (mFilteredNetworks.size() == 0) { 547 return null; 548 } 549 550 // Go through the registered network evaluators from the highest priority 551 // one to the lowest till a network is selected. 552 WifiConfiguration selectedNetwork = null; 553 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 554 if (registeredEvaluator != null) { 555 localLog("About to run " + registeredEvaluator.getName() + " :"); 556 selectedNetwork = registeredEvaluator.evaluateNetworks( 557 new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected, 558 untrustedNetworkAllowed, mConnectableNetworks); 559 if (selectedNetwork != null) { 560 localLog(registeredEvaluator.getName() + " selects " 561 + WifiNetworkSelector.toNetworkString(selectedNetwork) + " : " 562 + selectedNetwork.getNetworkSelectionStatus().getCandidate().BSSID); 563 break; 564 } 565 } 566 } 567 568 if (selectedNetwork != null) { 569 selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork); 570 mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis(); 571 } 572 573 return selectedNetwork; 574 } 575 576 /** 577 * Register a network evaluator 578 * 579 * @param evaluator the network evaluator to be registered 580 * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1) 581 * 582 * @return true if the evaluator is successfully registered with QNS; 583 * false if failed to register the evaluator 584 */ 585 public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) { 586 if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) { 587 localLog("Invalid network evaluator priority: " + priority); 588 return false; 589 } 590 591 if (mEvaluators[priority] != null) { 592 localLog("Priority " + priority + " is already registered by " 593 + mEvaluators[priority].getName()); 594 return false; 595 } 596 597 mEvaluators[priority] = evaluator; 598 return true; 599 } 600 601 WifiNetworkSelector(Context context, ScoringParams scoringParams, 602 WifiConfigManager configManager, Clock clock, 603 LocalLog localLog) { 604 mWifiConfigManager = configManager; 605 mClock = clock; 606 mScoringParams = scoringParams; 607 mLocalLog = localLog; 608 609 mEnableAutoJoinWhenAssociated = context.getResources().getBoolean( 610 R.bool.config_wifi_framework_enable_associated_network_selection); 611 mStayOnNetworkMinimumTxRate = context.getResources().getInteger( 612 R.integer.config_wifi_framework_min_tx_rate_for_staying_on_network); 613 mStayOnNetworkMinimumRxRate = context.getResources().getInteger( 614 R.integer.config_wifi_framework_min_rx_rate_for_staying_on_network); 615 } 616} 617