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