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