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