WifiNetworkSelector.java revision 98f550c5246c01ce6fd3bb936489dd69d15a74f3
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 final int mThresholdQualifiedRssi24; 60 private final int mThresholdQualifiedRssi5; 61 private final int mThresholdMinimumRssi24; 62 private final int mThresholdMinimumRssi5; 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 // Ephemeral network is not qualified. 149 if (network.ephemeral) { 150 localLog("Current network is an ephemeral one."); 151 return false; 152 } 153 154 // Open network is not qualified. 155 if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) { 156 localLog("Current network is a open one."); 157 return false; 158 } 159 160 int currentRssi = wifiInfo.getRssi(); 161 if (wifiInfo.is24GHz()) { 162 // 2.4GHz networks is not qualified whenever 5GHz is available 163 if (is5GHzNetworkAvailable(scanDetails)) { 164 localLog("Current network is 2.4GHz. 5GHz networks available."); 165 return false; 166 } 167 // When 5GHz is not available, we go through normal 2.4GHz qualification 168 if (currentRssi < mThresholdQualifiedRssi24) { 169 localLog("Current network band=2.4GHz, RSSI[" 170 + currentRssi + "]-acceptable but not qualified."); 171 return false; 172 } 173 } else if (wifiInfo.is5GHz()) { 174 // Must be 5GHz, so we always apply qualification checks 175 if (currentRssi < mThresholdQualifiedRssi5) { 176 localLog("Current network band=5GHz, RSSI[" 177 + currentRssi + "]-acceptable but not qualified."); 178 return false; 179 } 180 } else { 181 Log.e(TAG, "We're on a wifi network that's neither 2.4 or 5GHz... aliens!"); 182 return false; 183 } 184 185 return true; 186 } 187 188 // Determine whether there are any 5GHz networks in the scan result 189 private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) { 190 for (ScanDetail detail : scanDetails) { 191 ScanResult result = detail.getScanResult(); 192 if (result.is5GHz()) return true; 193 } 194 return false; 195 } 196 197 private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, 198 boolean connected, boolean disconnected) { 199 if (scanDetails.size() == 0) { 200 localLog("Empty connectivity scan results. Skip network selection."); 201 return false; 202 } 203 204 if (connected) { 205 // Is roaming allowed? 206 if (!mEnableAutoJoinWhenAssociated) { 207 localLog("Switching networks in connected state is not allowed." 208 + " Skip network selection."); 209 return false; 210 } 211 212 // Has it been at least the minimum interval since last network selection? 213 if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) { 214 long gap = mClock.getElapsedSinceBootMillis() 215 - mLastNetworkSelectionTimeStamp; 216 if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) { 217 localLog("Too short since last network selection: " + gap + " ms." 218 + " Skip network selection."); 219 return false; 220 } 221 } 222 223 if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) { 224 localLog("Current connected network already sufficient. Skip network selection."); 225 return false; 226 } else { 227 localLog("Current connected network is not sufficient."); 228 return true; 229 } 230 } else if (disconnected) { 231 return true; 232 } else { 233 // No network selection if WifiStateMachine is in a state other than 234 // CONNECTED or DISCONNECTED. 235 localLog("WifiStateMachine is in neither CONNECTED nor DISCONNECTED state." 236 + " Skip network selection."); 237 return false; 238 } 239 } 240 241 /** 242 * Format the given ScanResult as a scan ID for logging. 243 */ 244 public static String toScanId(@Nullable ScanResult scanResult) { 245 return scanResult == null ? "NULL" 246 : String.format("%s:%s", scanResult.SSID, scanResult.BSSID); 247 } 248 249 /** 250 * Format the given WifiConfiguration as a SSID:netId string 251 */ 252 public static String toNetworkString(WifiConfiguration network) { 253 if (network == null) { 254 return null; 255 } 256 257 return (network.SSID + ":" + network.networkId); 258 } 259 260 private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails, 261 HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) { 262 ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>(); 263 List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>(); 264 StringBuffer noValidSsid = new StringBuffer(); 265 StringBuffer blacklistedBssid = new StringBuffer(); 266 StringBuffer lowRssi = new StringBuffer(); 267 boolean scanResultsHaveCurrentBssid = false; 268 269 for (ScanDetail scanDetail : scanDetails) { 270 ScanResult scanResult = scanDetail.getScanResult(); 271 272 if (TextUtils.isEmpty(scanResult.SSID)) { 273 noValidSsid.append(scanResult.BSSID).append(" / "); 274 continue; 275 } 276 277 // Check if the scan results contain the currently connected BSSID 278 if (scanResult.BSSID.equals(currentBssid)) { 279 scanResultsHaveCurrentBssid = true; 280 } 281 282 final String scanId = toScanId(scanResult); 283 284 if (bssidBlacklist.contains(scanResult.BSSID)) { 285 blacklistedBssid.append(scanId).append(" / "); 286 continue; 287 } 288 289 // Skip network with too weak signals. 290 if ((scanResult.is24GHz() && scanResult.level 291 < mThresholdMinimumRssi24) 292 || (scanResult.is5GHz() && scanResult.level 293 < mThresholdMinimumRssi5)) { 294 lowRssi.append(scanId).append("(") 295 .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz") 296 .append(")").append(scanResult.level).append(" / "); 297 continue; 298 } 299 300 validScanDetails.add(scanDetail); 301 } 302 303 // WNS listens to all single scan results. Some scan requests may not include 304 // the channel of the currently connected network, so the currently connected 305 // network won't show up in the scan results. We don't act on these scan results 306 // to avoid aggressive network switching which might trigger disconnection. 307 if (isConnected && !scanResultsHaveCurrentBssid) { 308 localLog("Current connected BSSID " + currentBssid + " is not in the scan results." 309 + " Skip network selection."); 310 validScanDetails.clear(); 311 return validScanDetails; 312 } 313 314 if (noValidSsid.length() != 0) { 315 localLog("Networks filtered out due to invalid SSID: " + noValidSsid); 316 } 317 318 if (blacklistedBssid.length() != 0) { 319 localLog("Networks filtered out due to blacklist: " + blacklistedBssid); 320 } 321 322 if (lowRssi.length() != 0) { 323 localLog("Networks filtered out due to low signal strength: " + lowRssi); 324 } 325 326 return validScanDetails; 327 } 328 329 /** 330 * @return the list of ScanDetails scored as potential candidates by the last run of 331 * selectNetwork, this will be empty if Network selector determined no selection was 332 * needed on last run. This includes scan details of sufficient signal strength, and 333 * had an associated WifiConfiguration. 334 */ 335 public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() { 336 return mConnectableNetworks; 337 } 338 339 /** 340 * This API is called when user explicitly selects a network. Currently, it is used in following 341 * cases: 342 * (1) User explicitly chooses to connect to a saved network. 343 * (2) User saves a network after adding a new network. 344 * (3) User saves a network after modifying a saved network. 345 * Following actions will be triggered: 346 * 1. If this network is disabled, we need re-enable it again. 347 * 2. This network is favored over all the other networks visible in latest network 348 * selection procedure. 349 * 350 * @param netId ID for the network chosen by the user 351 * @return true -- There is change made to connection choice of any saved network. 352 * false -- There is no change made to connection choice of any saved network. 353 */ 354 public boolean setUserConnectChoice(int netId) { 355 localLog("userSelectNetwork: network ID=" + netId); 356 WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId); 357 358 if (selected == null || selected.SSID == null) { 359 localLog("userSelectNetwork: Invalid configuration with nid=" + netId); 360 return false; 361 } 362 363 // Enable the network if it is disabled. 364 if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { 365 mWifiConfigManager.updateNetworkSelectionStatus(netId, 366 WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE); 367 } 368 369 boolean change = false; 370 String key = selected.configKey(); 371 // This is only used for setting the connect choice timestamp for debugging purposes. 372 long currentTime = mClock.getWallClockMillis(); 373 List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(); 374 375 for (WifiConfiguration network : savedNetworks) { 376 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 377 if (network.networkId == selected.networkId) { 378 if (status.getConnectChoice() != null) { 379 localLog("Remove user selection preference of " + status.getConnectChoice() 380 + " Set Time: " + status.getConnectChoiceTimestamp() + " from " 381 + network.SSID + " : " + network.networkId); 382 mWifiConfigManager.clearNetworkConnectChoice(network.networkId); 383 change = true; 384 } 385 continue; 386 } 387 388 if (status.getSeenInLastQualifiedNetworkSelection() 389 && (status.getConnectChoice() == null 390 || !status.getConnectChoice().equals(key))) { 391 localLog("Add key: " + key + " Set Time: " + currentTime + " to " 392 + toNetworkString(network)); 393 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime); 394 change = true; 395 } 396 } 397 398 return change; 399 } 400 401 /** 402 * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen 403 * {@link WifiConfiguration} if one exists. 404 * 405 * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise 406 */ 407 private WifiConfiguration overrideCandidateWithUserConnectChoice( 408 @NonNull WifiConfiguration candidate) { 409 WifiConfiguration tempConfig = candidate; 410 WifiConfiguration originalCandidate = 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 430 if (candidate != originalCandidate) { 431 localLog("After user selection adjustment, the final candidate is:" 432 + WifiNetworkSelector.toNetworkString(candidate) + " : " 433 + scanResultCandidate.BSSID); 434 } 435 return candidate; 436 } 437 438 /** 439 * Select the best network from the ones in range. 440 * 441 * @param scanDetails List of ScanDetail for all the APs in range 442 * @param bssidBlacklist Blacklisted BSSIDs 443 * @param wifiInfo Currently connected network 444 * @param connected True if the device is connected 445 * @param disconnected True if the device is disconnected 446 * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection 447 * @return Configuration of the selected network, or Null if nothing 448 */ 449 @Nullable 450 public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, 451 HashSet<String> bssidBlacklist, WifiInfo wifiInfo, 452 boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) { 453 mConnectableNetworks.clear(); 454 if (scanDetails.size() == 0) { 455 localLog("Empty connectivity scan result"); 456 return null; 457 } 458 459 WifiConfiguration currentNetwork = 460 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 461 462 // Always get the current BSSID from WifiInfo in case that firmware initiated 463 // roaming happened. 464 String currentBssid = wifiInfo.getBSSID(); 465 466 // Shall we start network selection at all? 467 if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { 468 return null; 469 } 470 471 // Update the registered network evaluators. 472 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 473 if (registeredEvaluator != null) { 474 registeredEvaluator.update(scanDetails); 475 } 476 } 477 478 // Filter out unwanted networks. 479 List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, bssidBlacklist, 480 connected, currentBssid); 481 if (filteredScanDetails.size() == 0) { 482 return null; 483 } 484 485 // Go through the registered network evaluators from the highest priority 486 // one to the lowest till a network is selected. 487 WifiConfiguration selectedNetwork = null; 488 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 489 if (registeredEvaluator != null) { 490 localLog("About to run " + registeredEvaluator.getName() + " :"); 491 selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails, 492 currentNetwork, currentBssid, connected, 493 untrustedNetworkAllowed, mConnectableNetworks); 494 if (selectedNetwork != null) { 495 localLog(registeredEvaluator.getName() + " selects " 496 + WifiNetworkSelector.toNetworkString(selectedNetwork) + " : " 497 + selectedNetwork.getNetworkSelectionStatus().getCandidate().BSSID); 498 break; 499 } 500 } 501 } 502 503 if (selectedNetwork != null) { 504 selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork); 505 mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis(); 506 } 507 508 return selectedNetwork; 509 } 510 511 /** 512 * Register a network evaluator 513 * 514 * @param evaluator the network evaluator to be registered 515 * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1) 516 * 517 * @return true if the evaluator is successfully registered with QNS; 518 * false if failed to register the evaluator 519 */ 520 public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) { 521 if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) { 522 localLog("Invalid network evaluator priority: " + priority); 523 return false; 524 } 525 526 if (mEvaluators[priority] != null) { 527 localLog("Priority " + priority + " is already registered by " 528 + mEvaluators[priority].getName()); 529 return false; 530 } 531 532 mEvaluators[priority] = evaluator; 533 return true; 534 } 535 536 WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock, 537 LocalLog localLog) { 538 mWifiConfigManager = configManager; 539 mClock = clock; 540 mLocalLog = localLog; 541 542 mThresholdQualifiedRssi24 = context.getResources().getInteger( 543 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz); 544 mThresholdQualifiedRssi5 = context.getResources().getInteger( 545 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz); 546 mThresholdMinimumRssi24 = context.getResources().getInteger( 547 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz); 548 mThresholdMinimumRssi5 = context.getResources().getInteger( 549 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz); 550 mEnableAutoJoinWhenAssociated = context.getResources().getBoolean( 551 R.bool.config_wifi_framework_enable_associated_network_selection); 552 } 553} 554