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