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