WifiNetworkSelector.java revision 235642dba4359c1e68618f27c949e744765cbbcc
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.app.ActivityManager; 22import android.content.Context; 23import android.net.NetworkKey; 24import android.net.wifi.ScanResult; 25import android.net.wifi.WifiConfiguration; 26import android.net.wifi.WifiInfo; 27import android.text.TextUtils; 28import android.util.LocalLog; 29import android.util.Pair; 30 31import com.android.internal.R; 32import com.android.internal.annotations.VisibleForTesting; 33 34import java.io.FileDescriptor; 35import java.io.PrintWriter; 36import java.util.ArrayList; 37import java.util.HashMap; 38import java.util.Iterator; 39import java.util.List; 40import java.util.Map; 41 42/** 43 * This class looks at all the connectivity scan results then 44 * selects a network for the phone to connect or roam to. 45 */ 46public class WifiNetworkSelector { 47 private static final long INVALID_TIME_STAMP = Long.MIN_VALUE; 48 // Minimum time gap between last successful network selection and a new selection 49 // attempt. 50 @VisibleForTesting 51 public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000; 52 53 // Constants for BSSID blacklist. 54 public static final int BSSID_BLACKLIST_THRESHOLD = 3; 55 public static final int BSSID_BLACKLIST_EXPIRE_TIME_MS = 5 * 60 * 1000; 56 57 // Association success/failure reason codes 58 @VisibleForTesting 59 public static final int REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA = 17; 60 61 private WifiConfigManager mWifiConfigManager; 62 private Clock mClock; 63 private static class BssidBlacklistStatus { 64 // Number of times this BSSID has been rejected for association. 65 public int counter; 66 public boolean isBlacklisted; 67 public long blacklistedTimeStamp = INVALID_TIME_STAMP; 68 } 69 private Map<String, BssidBlacklistStatus> mBssidBlacklist = 70 new HashMap<>(); 71 72 private final LocalLog mLocalLog = 73 new LocalLog(ActivityManager.isLowRamDeviceStatic() ? 256 : 512); 74 private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP; 75 // Buffer of filtered scan results (Scan results considered by network selection) & associated 76 // WifiConfiguration (if any). 77 private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks = 78 new ArrayList<>(); 79 private final int mThresholdQualifiedRssi24; 80 private final int mThresholdQualifiedRssi5; 81 private final int mThresholdMinimumRssi24; 82 private final int mThresholdMinimumRssi5; 83 private final boolean mEnableAutoJoinWhenAssociated; 84 85 /** 86 * WiFi Network Selector supports various types of networks. Each type can 87 * have its evaluator to choose the best WiFi network for the device to connect 88 * to. When registering a WiFi network evaluator with the WiFi Network Selector, 89 * the priority of the network must be specified, and it must be a value between 90 * 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi 91 * Network Selector iterates through the registered scorers from the highest priority 92 * to the lowest till a network is selected. 93 */ 94 public static final int EVALUATOR_MIN_PRIORITY = 6; 95 96 /** 97 * Maximum number of evaluators can be registered with Wifi Network Selector. 98 */ 99 public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY; 100 101 /** 102 * Interface for WiFi Network Evaluator 103 * 104 * A network scorer evaulates all the networks from the scan results and 105 * recommends the best network in its category to connect or roam to. 106 */ 107 public interface NetworkEvaluator { 108 /** 109 * Get the evaluator name. 110 */ 111 String getName(); 112 113 /** 114 * Update the evaluator. 115 * 116 * Certain evaluators have to be updated with the new scan results. For example 117 * the ExternalScoreEvalutor needs to refresh its Score Cache. 118 * 119 * @param scanDetails a list of scan details constructed from the scan results 120 */ 121 void update(List<ScanDetail> scanDetails); 122 123 /** 124 * Evaluate all the networks from the scan results. 125 * 126 * @param scanDetails a list of scan details constructed from the scan results 127 * @param currentNetwork configuration of the current connected network 128 * or null if disconnected 129 * @param currentBssid BSSID of the current connected network or null if 130 * disconnected 131 * @param connected a flag to indicate if WifiStateMachine is in connected 132 * state 133 * @param untrustedNetworkAllowed a flag to indidate if untrusted networks like 134 * ephemeral networks are allowed 135 * @param connectableNetworks a list of the ScanDetail and WifiConfiguration 136 * pair which is used by the WifiLastResortWatchdog 137 * @return configuration of the chosen network; 138 * null if no network in this category is available. 139 */ 140 @Nullable 141 WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails, 142 WifiConfiguration currentNetwork, String currentBssid, 143 boolean connected, boolean untrustedNetworkAllowed, 144 List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks); 145 } 146 147 private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS]; 148 149 // A helper to log debugging information in the local log buffer, which can 150 // be retrieved in bugreport. 151 private void localLog(String log) { 152 mLocalLog.log(log); 153 } 154 155 private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo) { 156 WifiConfiguration network = 157 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 158 159 // Currently connected? 160 if (network == null) { 161 localLog("No current connected network."); 162 return false; 163 } else { 164 localLog("Current connected network: " + network.SSID 165 + " , ID: " + network.networkId); 166 } 167 168 // Ephemeral network is not qualified. 169 if (network.ephemeral) { 170 localLog("Current network is an ephemeral one."); 171 return false; 172 } 173 174 // Open network is not qualified. 175 if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) { 176 localLog("Current network is a open one."); 177 return false; 178 } 179 180 // 2.4GHz networks is not qualified. 181 if (wifiInfo.is24GHz()) { 182 localLog("Current network is 2.4GHz."); 183 return false; 184 } 185 186 // Is the current network's singnal strength qualified? It can only 187 // be a 5GHz network if we reach here. 188 int currentRssi = wifiInfo.getRssi(); 189 if (wifiInfo.is5GHz() && currentRssi < mThresholdQualifiedRssi5) { 190 localLog("Current network band=" + (wifiInfo.is5GHz() ? "5GHz" : "2.4GHz") 191 + ", RSSI[" + currentRssi + "]-acceptable but not qualified."); 192 return false; 193 } 194 195 return true; 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)) { 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, boolean isConnected, 262 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 (isBssidDisabled(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 * @return the list of ScanDetails scored as potential candidates by the last run of 332 * selectNetwork, this will be empty if Network selector determined no selection was 333 * needed on last run. This includes scan details of sufficient signal strength, and 334 * had an associated WifiConfiguration. 335 */ 336 public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() { 337 return mConnectableNetworks; 338 } 339 340 /** 341 * This API is called when user explicitly selects a network. Currently, it is used in following 342 * cases: 343 * (1) User explicitly chooses to connect to a saved network. 344 * (2) User saves a network after adding a new network. 345 * (3) User saves a network after modifying a saved network. 346 * Following actions will be triggered: 347 * 1. If this network is disabled, we need re-enable it again. 348 * 2. This network is favored over all the other networks visible in latest network 349 * selection procedure. 350 * 351 * @param netId ID for the network chosen by the user 352 * @return true -- There is change made to connection choice of any saved network. 353 * false -- There is no change made to connection choice of any saved network. 354 */ 355 public boolean setUserConnectChoice(int netId) { 356 localLog("userSelectNetwork: network ID=" + netId); 357 WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId); 358 359 if (selected == null || selected.SSID == null) { 360 localLog("userSelectNetwork: Invalid configuration with nid=" + netId); 361 return false; 362 } 363 364 // Enable the network if it is disabled. 365 if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { 366 mWifiConfigManager.updateNetworkSelectionStatus(netId, 367 WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE); 368 } 369 370 boolean change = false; 371 String key = selected.configKey(); 372 // This is only used for setting the connect choice timestamp for debugging purposes. 373 long currentTime = mClock.getWallClockMillis(); 374 List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(); 375 376 for (WifiConfiguration network : savedNetworks) { 377 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 378 if (network.networkId == selected.networkId) { 379 if (status.getConnectChoice() != null) { 380 localLog("Remove user selection preference of " + status.getConnectChoice() 381 + " Set Time: " + status.getConnectChoiceTimestamp() + " from " 382 + network.SSID + " : " + network.networkId); 383 mWifiConfigManager.clearNetworkConnectChoice(network.networkId); 384 change = true; 385 } 386 continue; 387 } 388 389 if (status.getSeenInLastQualifiedNetworkSelection() 390 && (status.getConnectChoice() == null 391 || !status.getConnectChoice().equals(key))) { 392 localLog("Add key: " + key + " Set Time: " + currentTime + " to " 393 + toNetworkString(network)); 394 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime); 395 change = true; 396 } 397 } 398 399 return change; 400 } 401 402 /** 403 * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen 404 * {@link WifiConfiguration} if one exists. 405 * 406 * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise 407 */ 408 private WifiConfiguration overrideCandidateWithUserConnectChoice( 409 @NonNull WifiConfiguration candidate) { 410 WifiConfiguration tempConfig = candidate; 411 ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate(); 412 413 while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) { 414 String key = tempConfig.getNetworkSelectionStatus().getConnectChoice(); 415 tempConfig = mWifiConfigManager.getConfiguredNetwork(key); 416 417 if (tempConfig != null) { 418 WifiConfiguration.NetworkSelectionStatus tempStatus = 419 tempConfig.getNetworkSelectionStatus(); 420 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) { 421 scanResultCandidate = tempStatus.getCandidate(); 422 candidate = tempConfig; 423 } 424 } else { 425 localLog("Connect choice: " + key + " has no corresponding saved config."); 426 break; 427 } 428 } 429 localLog("After user selection adjustment, the final candidate is:" 430 + WifiNetworkSelector.toNetworkString(candidate) + " : " 431 + scanResultCandidate.BSSID); 432 return candidate; 433 } 434 435 /** 436 * Enable/disable a BSSID for Network Selection 437 * When an association rejection event is obtained, Network Selector will disable this 438 * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it 439 * successfully later, this bssid can be re-enabled. 440 * 441 * @param bssid the bssid to be enabled / disabled 442 * @param enable -- true enable a bssid if it has been disabled 443 * -- false disable a bssid 444 * @param reasonCode enable/disable reason code 445 */ 446 public boolean enableBssidForNetworkSelection(String bssid, boolean enable, int reasonCode) { 447 if (enable) { 448 return (mBssidBlacklist.remove(bssid) != null); 449 } else { 450 if (bssid != null) { 451 BssidBlacklistStatus status = mBssidBlacklist.get(bssid); 452 if (status == null) { 453 // First time for this BSSID 454 status = new BssidBlacklistStatus(); 455 mBssidBlacklist.put(bssid, status); 456 } 457 458 if (!status.isBlacklisted) { 459 status.counter++; 460 if (status.counter >= BSSID_BLACKLIST_THRESHOLD 461 || reasonCode == REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA) { 462 status.isBlacklisted = true; 463 status.blacklistedTimeStamp = mClock.getElapsedSinceBootMillis(); 464 return true; 465 } 466 } 467 } 468 } 469 return false; 470 } 471 472 /** 473 * Update the BSSID blacklist 474 * 475 * Go through the BSSID blacklist and check when a BSSID was blocked. If it 476 * has been blacklisted for BSSID_BLACKLIST_EXPIRE_TIME_MS, then re-enable it. 477 */ 478 private void updateBssidBlacklist() { 479 Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator(); 480 while (iter.hasNext()) { 481 BssidBlacklistStatus status = iter.next(); 482 if (status != null && status.isBlacklisted) { 483 if (mClock.getElapsedSinceBootMillis() - status.blacklistedTimeStamp 484 >= BSSID_BLACKLIST_EXPIRE_TIME_MS) { 485 iter.remove(); 486 } 487 } 488 } 489 } 490 491 /** 492 * Check whether a bssid is disabled 493 * @param bssid -- the bssid to check 494 */ 495 private boolean isBssidDisabled(String bssid) { 496 BssidBlacklistStatus status = mBssidBlacklist.get(bssid); 497 return status == null ? false : status.isBlacklisted; 498 } 499 500 /** 501 * 502 */ 503 @Nullable 504 public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, WifiInfo wifiInfo, 505 boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) { 506 mConnectableNetworks.clear(); 507 if (scanDetails.size() == 0) { 508 localLog("Empty connectivity scan result"); 509 return null; 510 } 511 512 WifiConfiguration currentNetwork = 513 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 514 515 // Always get the current BSSID from WifiInfo in case that firmware initiated 516 // roaming happened. 517 String currentBssid = wifiInfo.getBSSID(); 518 519 // Shall we start network selection at all? 520 if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { 521 return null; 522 } 523 524 // Update the registered network evaluators. 525 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 526 if (registeredEvaluator != null) { 527 registeredEvaluator.update(scanDetails); 528 } 529 } 530 531 // Check if any BSSID can be freed from the blacklist. 532 updateBssidBlacklist(); 533 534 // Filter out unwanted networks. 535 List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, connected, 536 currentBssid); 537 if (filteredScanDetails.size() == 0) { 538 return null; 539 } 540 541 // Go through the registered network evaluators from the highest priority 542 // one to the lowest till a network is selected. 543 WifiConfiguration selectedNetwork = null; 544 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 545 if (registeredEvaluator != null) { 546 selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails, 547 currentNetwork, currentBssid, connected, 548 untrustedNetworkAllowed, mConnectableNetworks); 549 if (selectedNetwork != null) { 550 break; 551 } 552 } 553 } 554 555 if (selectedNetwork != null) { 556 selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork); 557 mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis(); 558 } 559 560 return selectedNetwork; 561 } 562 563 /** 564 * Register a network evaluator 565 * 566 * @param evaluator the network evaluator to be registered 567 * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1) 568 * 569 * @return true if the evaluator is successfully registered with QNS; 570 * false if failed to register the evaluator 571 */ 572 public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) { 573 if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) { 574 localLog("Invalid network evaluator priority: " + priority); 575 return false; 576 } 577 578 if (mEvaluators[priority] != null) { 579 localLog("Priority " + priority + " is already registered by " 580 + mEvaluators[priority].getName()); 581 return false; 582 } 583 584 mEvaluators[priority] = evaluator; 585 return true; 586 } 587 588 WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock) { 589 mWifiConfigManager = configManager; 590 mClock = clock; 591 592 mThresholdQualifiedRssi24 = context.getResources().getInteger( 593 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz); 594 mThresholdQualifiedRssi5 = context.getResources().getInteger( 595 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz); 596 mThresholdMinimumRssi24 = context.getResources().getInteger( 597 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz); 598 mThresholdMinimumRssi5 = context.getResources().getInteger( 599 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz); 600 mEnableAutoJoinWhenAssociated = context.getResources().getBoolean( 601 R.bool.config_wifi_framework_enable_associated_network_selection); 602 } 603 604 /** 605 * Retrieve the local log buffer created by WifiNetworkSelector. 606 */ 607 public LocalLog getLocalLog() { 608 return mLocalLog; 609 } 610 611 /** 612 * Dump the local logs. 613 */ 614 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 615 pw.println("Dump of WifiNetworkSelector"); 616 pw.println("WifiNetworkSelector - Log Begin ----"); 617 mLocalLog.dump(fd, pw, args); 618 pw.println("WifiNetworkSelector - Log End ----"); 619 } 620} 621