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