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 static android.net.wifi.WifiManager.WIFI_FEATURE_OWE; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.Context; 25 import android.net.MacAddress; 26 import android.net.wifi.ScanResult; 27 import android.net.wifi.SupplicantState; 28 import android.net.wifi.WifiConfiguration; 29 import android.net.wifi.WifiInfo; 30 import android.telephony.TelephonyManager; 31 import android.text.TextUtils; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import android.util.LocalLog; 35 import android.util.Log; 36 import android.util.Pair; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.Preconditions; 40 import com.android.server.wifi.hotspot2.NetworkDetail; 41 import com.android.server.wifi.proto.nano.WifiMetricsProto; 42 import com.android.server.wifi.util.InformationElementUtil.BssLoad; 43 import com.android.server.wifi.util.ScanResultUtil; 44 import com.android.wifi.resources.R; 45 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.RetentionPolicy; 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.concurrent.TimeUnit; 54 import java.util.stream.Collectors; 55 56 /** 57 * WifiNetworkSelector looks at all the connectivity scan results and 58 * runs all the nominators to find or create matching configurations. 59 * Then it makes a final selection from among the resulting candidates. 60 */ 61 public class WifiNetworkSelector { 62 private static final String TAG = "WifiNetworkSelector"; 63 64 private static final long INVALID_TIME_STAMP = Long.MIN_VALUE; 65 66 /** 67 * Minimum time gap between last successful network selection and a 68 * new selection attempt. 69 */ 70 @VisibleForTesting 71 public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000; 72 73 /** 74 * Connected score value used to decide whether a still-connected wifi should be treated 75 * as unconnected when filtering scan results. 76 */ 77 @VisibleForTesting 78 public static final int WIFI_POOR_SCORE = ConnectedScore.WIFI_TRANSITION_SCORE - 10; 79 80 /** 81 * The identifier string of the CandidateScorer to use (in the absence of overrides). 82 */ 83 public static final String PRESET_CANDIDATE_SCORER_NAME = "ThroughputScorer"; 84 85 /** 86 * Experiment ID for the legacy scorer. 87 */ 88 public static final int LEGACY_CANDIDATE_SCORER_EXP_ID = 0; 89 90 private final Context mContext; 91 private final WifiConfigManager mWifiConfigManager; 92 private final Clock mClock; 93 private final LocalLog mLocalLog; 94 private final WifiMetrics mWifiMetrics; 95 private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP; 96 // Buffer of filtered scan results (Scan results considered by network selection) & associated 97 // WifiConfiguration (if any). 98 private final List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks = 99 new ArrayList<>(); 100 private List<ScanDetail> mFilteredNetworks = new ArrayList<>(); 101 private final WifiScoreCard mWifiScoreCard; 102 private final ScoringParams mScoringParams; 103 private final WifiNative mWifiNative; 104 105 private final Map<String, WifiCandidates.CandidateScorer> mCandidateScorers = new ArrayMap<>(); 106 private boolean mIsEnhancedOpenSupportedInitialized = false; 107 private boolean mIsEnhancedOpenSupported; 108 private ThroughputPredictor mThroughputPredictor; 109 private boolean mIsBluetoothConnected = false; 110 private WifiChannelUtilization mWifiChannelUtilization; 111 112 /** 113 * Interface for WiFi Network Nominator 114 * 115 * A network nominator examines the scan results reports the 116 * connectable candidates in its category for further consideration. 117 */ 118 public interface NetworkNominator { 119 /** Type of nominators */ 120 int NOMINATOR_ID_SAVED = 0; 121 int NOMINATOR_ID_SUGGESTION = 1; 122 int NOMINATOR_ID_SCORED = 4; 123 int NOMINATOR_ID_CURRENT = 5; // Should always be last 124 125 @IntDef(prefix = {"NOMINATOR_ID_"}, value = { 126 NOMINATOR_ID_SAVED, 127 NOMINATOR_ID_SUGGESTION, 128 NOMINATOR_ID_SCORED, 129 NOMINATOR_ID_CURRENT}) 130 @Retention(RetentionPolicy.SOURCE) 131 public @interface NominatorId { 132 } 133 134 /** 135 * Get the nominator type. 136 */ 137 @NominatorId getId()138 int getId(); 139 140 /** 141 * Get the nominator name. 142 */ getName()143 String getName(); 144 145 /** 146 * Update the nominator. 147 * 148 * Certain nominators have to be updated with the new scan results. For example 149 * the ScoredNetworkNominator needs to refresh its Score Cache. 150 * 151 * @param scanDetails a list of scan details constructed from the scan results 152 */ update(List<ScanDetail> scanDetails)153 void update(List<ScanDetail> scanDetails); 154 155 /** 156 * Evaluate all the networks from the scan results. 157 * 158 * @param scanDetails a list of scan details constructed from the scan results 159 * @param currentNetwork configuration of the current connected network 160 * or null if disconnected 161 * @param currentBssid BSSID of the current connected network or null if 162 * disconnected 163 * @param connected a flag to indicate if ClientModeImpl is in connected 164 * state 165 * @param untrustedNetworkAllowed a flag to indicate if untrusted networks like 166 * ephemeral networks are allowed 167 * @param onConnectableListener callback to record all of the connectable networks 168 */ nominateNetworks(List<ScanDetail> scanDetails, WifiConfiguration currentNetwork, String currentBssid, boolean connected, boolean untrustedNetworkAllowed, OnConnectableListener onConnectableListener)169 void nominateNetworks(List<ScanDetail> scanDetails, 170 WifiConfiguration currentNetwork, String currentBssid, 171 boolean connected, boolean untrustedNetworkAllowed, 172 OnConnectableListener onConnectableListener); 173 174 /** 175 * Callback for recording connectable candidates 176 */ 177 public interface OnConnectableListener { 178 /** 179 * Notes that an access point is an eligible connection candidate 180 * 181 * @param scanDetail describes the specific access point 182 * @param config is the WifiConfiguration for the network 183 */ onConnectable(ScanDetail scanDetail, WifiConfiguration config)184 void onConnectable(ScanDetail scanDetail, WifiConfiguration config); 185 } 186 } 187 188 private final List<NetworkNominator> mNominators = new ArrayList<>(3); 189 190 // A helper to log debugging information in the local log buffer, which can 191 // be retrieved in bugreport. localLog(String log)192 private void localLog(String log) { 193 mLocalLog.log(log); 194 } 195 196 /** 197 * Check if current network has sufficient RSSI 198 * 199 * @param wifiInfo info of currently connected network 200 * @return true if current link quality is sufficient, false otherwise. 201 */ hasSufficientLinkQuality(WifiInfo wifiInfo)202 public boolean hasSufficientLinkQuality(WifiInfo wifiInfo) { 203 int currentRssi = wifiInfo.getRssi(); 204 return currentRssi >= mScoringParams.getSufficientRssi(wifiInfo.getFrequency()); 205 } 206 207 /** 208 * Check if current network has active Tx or Rx traffic 209 * 210 * @param wifiInfo info of currently connected network 211 * @return true if it has active Tx or Rx traffic, false otherwise. 212 */ hasActiveStream(WifiInfo wifiInfo)213 public boolean hasActiveStream(WifiInfo wifiInfo) { 214 return wifiInfo.getSuccessfulTxPacketsPerSecond() 215 > mScoringParams.getActiveTrafficPacketsPerSecond() 216 || wifiInfo.getSuccessfulRxPacketsPerSecond() 217 > mScoringParams.getActiveTrafficPacketsPerSecond(); 218 } 219 220 /** 221 * Check if current network has internet or is expected to not have internet 222 */ hasInternetOrExpectNoInternet(WifiInfo wifiInfo)223 public boolean hasInternetOrExpectNoInternet(WifiInfo wifiInfo) { 224 WifiConfiguration network = 225 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 226 if (network == null) { 227 return false; 228 } 229 return !network.hasNoInternetAccess() || network.isNoInternetAccessExpected(); 230 } 231 /** 232 * Determines whether the currently connected network is sufficient. 233 * 234 * If the network is good enough, or if switching to a new network is likely to 235 * be disruptive, we should avoid doing a network selection. 236 * 237 * @param wifiInfo info of currently connected network 238 * @return true if the network is sufficient 239 */ isNetworkSufficient(WifiInfo wifiInfo)240 public boolean isNetworkSufficient(WifiInfo wifiInfo) { 241 // Currently connected? 242 if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) { 243 return false; 244 } 245 246 localLog("Current connected network: " + wifiInfo.getNetworkId()); 247 248 WifiConfiguration network = 249 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 250 251 if (network == null) { 252 localLog("Current network was removed"); 253 return false; 254 } 255 256 // Skip autojoin for the first few seconds of a user-initiated connection. 257 // This delays network selection during the time that connectivity service may be posting 258 // a dialog about a no-internet network. 259 if (mWifiConfigManager.getLastSelectedNetwork() == network.networkId 260 && (mClock.getElapsedSinceBootMillis() 261 - mWifiConfigManager.getLastSelectedTimeStamp()) 262 <= mContext.getResources().getInteger( 263 R.integer.config_wifiSufficientDurationAfterUserSelectionMilliseconds)) { 264 localLog("Current network is recently user-selected"); 265 return true; 266 } 267 268 // Set OSU (Online Sign Up) network for Passpoint Release 2 to sufficient 269 // so that network select selection is skipped and OSU process can complete. 270 if (network.osu) { 271 localLog("Current connection is OSU"); 272 return true; 273 } 274 275 // Network without internet access is not sufficient, unless expected 276 if (!hasInternetOrExpectNoInternet(wifiInfo)) { 277 localLog("Current network has [" + network.numNoInternetAccessReports 278 + "] no-internet access reports"); 279 return false; 280 } 281 282 if (!hasSufficientLinkQuality(wifiInfo)) { 283 localLog("Current network link quality is not sufficient"); 284 return false; 285 } 286 287 if (!hasActiveStream(wifiInfo)) { 288 localLog("Current network has low ongoing traffic"); 289 return false; 290 } 291 292 return true; 293 } 294 isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, boolean connected, boolean disconnected)295 private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, 296 boolean connected, boolean disconnected) { 297 if (scanDetails.size() == 0) { 298 localLog("Empty connectivity scan results. Skip network selection."); 299 return false; 300 } 301 302 if (connected) { 303 // Is roaming allowed? 304 if (!mContext.getResources().getBoolean( 305 R.bool.config_wifi_framework_enable_associated_network_selection)) { 306 localLog("Switching networks in connected state is not allowed." 307 + " Skip network selection."); 308 return false; 309 } 310 311 // Has it been at least the minimum interval since last network selection? 312 if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) { 313 long gap = mClock.getElapsedSinceBootMillis() 314 - mLastNetworkSelectionTimeStamp; 315 if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) { 316 localLog("Too short since last network selection: " + gap + " ms." 317 + " Skip network selection."); 318 return false; 319 } 320 } 321 // Please note other scans (e.g., location scan or app scan) may also trigger network 322 // selection and these scans may or may not run sufficiency check. 323 // So it is better to run sufficiency check here before network selection. 324 if (isNetworkSufficient(wifiInfo)) { 325 localLog("Current connected network already sufficient. Skip network selection."); 326 return false; 327 } else { 328 localLog("Current connected network is not sufficient."); 329 return true; 330 } 331 } else if (disconnected) { 332 return true; 333 } else { 334 // No network selection if ClientModeImpl is in a state other than 335 // CONNECTED or DISCONNECTED. 336 localLog("ClientModeImpl is in neither CONNECTED nor DISCONNECTED state." 337 + " Skip network selection."); 338 return false; 339 } 340 } 341 342 /** 343 * Format the given ScanResult as a scan ID for logging. 344 */ toScanId(@ullable ScanResult scanResult)345 public static String toScanId(@Nullable ScanResult scanResult) { 346 return scanResult == null ? "NULL" 347 : String.format("%s:%s", scanResult.SSID, scanResult.BSSID); 348 } 349 350 /** 351 * Format the given WifiConfiguration as a SSID:netId string 352 */ toNetworkString(WifiConfiguration network)353 public static String toNetworkString(WifiConfiguration network) { 354 if (network == null) { 355 return null; 356 } 357 358 return (network.SSID + ":" + network.networkId); 359 } 360 361 /** 362 * Compares ScanResult level against the minimum threshold for its band, returns true if lower 363 */ isSignalTooWeak(ScanResult scanResult)364 public boolean isSignalTooWeak(ScanResult scanResult) { 365 return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency)); 366 } 367 filterScanResults(List<ScanDetail> scanDetails, Set<String> bssidBlacklist, boolean isConnected, String currentBssid)368 private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails, 369 Set<String> bssidBlacklist, boolean isConnected, String currentBssid) { 370 List<ScanDetail> validScanDetails = new ArrayList<>(); 371 StringBuffer noValidSsid = new StringBuffer(); 372 StringBuffer blacklistedBssid = new StringBuffer(); 373 StringBuffer lowRssi = new StringBuffer(); 374 StringBuffer mboAssociationDisallowedBssid = new StringBuffer(); 375 boolean scanResultsHaveCurrentBssid = false; 376 int numBssidFiltered = 0; 377 378 for (ScanDetail scanDetail : scanDetails) { 379 ScanResult scanResult = scanDetail.getScanResult(); 380 381 if (TextUtils.isEmpty(scanResult.SSID)) { 382 noValidSsid.append(scanResult.BSSID).append(" / "); 383 continue; 384 } 385 386 // Check if the scan results contain the currently connected BSSID 387 if (scanResult.BSSID.equals(currentBssid)) { 388 scanResultsHaveCurrentBssid = true; 389 validScanDetails.add(scanDetail); 390 continue; 391 } 392 393 final String scanId = toScanId(scanResult); 394 395 if (bssidBlacklist.contains(scanResult.BSSID)) { 396 blacklistedBssid.append(scanId).append(" / "); 397 numBssidFiltered++; 398 continue; 399 } 400 401 // Skip network with too weak signals. 402 if (isSignalTooWeak(scanResult)) { 403 lowRssi.append(scanId); 404 if (scanResult.is24GHz()) { 405 lowRssi.append("(2.4GHz)"); 406 } else if (scanResult.is5GHz()) { 407 lowRssi.append("(5GHz)"); 408 } else if (scanResult.is6GHz()) { 409 lowRssi.append("(6GHz)"); 410 } 411 lowRssi.append(scanResult.level).append(" / "); 412 continue; 413 } 414 415 // Skip BSS which is not accepting new connections. 416 NetworkDetail networkDetail = scanDetail.getNetworkDetail(); 417 if (networkDetail != null) { 418 if (networkDetail.getMboAssociationDisallowedReasonCode() 419 != MboOceConstants.MBO_OCE_ATTRIBUTE_NOT_PRESENT) { 420 mWifiMetrics 421 .incrementNetworkSelectionFilteredBssidCountDueToMboAssocDisallowInd(); 422 mboAssociationDisallowedBssid.append(scanId).append("(") 423 .append(networkDetail.getMboAssociationDisallowedReasonCode()) 424 .append(")").append(" / "); 425 continue; 426 } 427 } 428 429 validScanDetails.add(scanDetail); 430 } 431 mWifiMetrics.incrementNetworkSelectionFilteredBssidCount(numBssidFiltered); 432 433 // WNS listens to all single scan results. Some scan requests may not include 434 // the channel of the currently connected network, so the currently connected 435 // network won't show up in the scan results. We don't act on these scan results 436 // to avoid aggressive network switching which might trigger disconnection. 437 // TODO(b/147751334) this may no longer be needed 438 if (isConnected && !scanResultsHaveCurrentBssid) { 439 localLog("Current connected BSSID " + currentBssid + " is not in the scan results." 440 + " Skip network selection."); 441 validScanDetails.clear(); 442 return validScanDetails; 443 } 444 445 if (noValidSsid.length() != 0) { 446 localLog("Networks filtered out due to invalid SSID: " + noValidSsid); 447 } 448 449 if (blacklistedBssid.length() != 0) { 450 localLog("Networks filtered out due to blocklist: " + blacklistedBssid); 451 } 452 453 if (lowRssi.length() != 0) { 454 localLog("Networks filtered out due to low signal strength: " + lowRssi); 455 } 456 457 if (mboAssociationDisallowedBssid.length() != 0) { 458 localLog("Networks filtered out due to mbo association disallowed indication: " 459 + mboAssociationDisallowedBssid); 460 } 461 462 return validScanDetails; 463 } 464 isEnhancedOpenSupported()465 private boolean isEnhancedOpenSupported() { 466 if (mIsEnhancedOpenSupportedInitialized) { 467 return mIsEnhancedOpenSupported; 468 } 469 470 mIsEnhancedOpenSupportedInitialized = true; 471 mIsEnhancedOpenSupported = (mWifiNative.getSupportedFeatureSet( 472 mWifiNative.getClientInterfaceName()) & WIFI_FEATURE_OWE) != 0; 473 return mIsEnhancedOpenSupported; 474 } 475 476 /** 477 * This returns a list of ScanDetails that were filtered in the process of network selection. 478 * The list is further filtered for only open unsaved networks. 479 * 480 * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS, 481 * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are 482 * no open unsaved networks, or when network selection has not been run. 483 */ getFilteredScanDetailsForOpenUnsavedNetworks()484 public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() { 485 List<ScanDetail> openUnsavedNetworks = new ArrayList<>(); 486 boolean enhancedOpenSupported = isEnhancedOpenSupported(); 487 for (ScanDetail scanDetail : mFilteredNetworks) { 488 ScanResult scanResult = scanDetail.getScanResult(); 489 490 if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) { 491 continue; 492 } 493 494 // Filter out Enhanced Open networks on devices that do not support it 495 if (ScanResultUtil.isScanResultForOweNetwork(scanResult) 496 && !enhancedOpenSupported) { 497 continue; 498 } 499 500 // Skip saved networks 501 if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) { 502 continue; 503 } 504 505 openUnsavedNetworks.add(scanDetail); 506 } 507 return openUnsavedNetworks; 508 } 509 510 /** 511 * @return the list of ScanDetails scored as potential candidates by the last run of 512 * selectNetwork, this will be empty if Network selector determined no selection was 513 * needed on last run. This includes scan details of sufficient signal strength, and 514 * had an associated WifiConfiguration. 515 */ getConnectableScanDetails()516 public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() { 517 return mConnectableNetworks; 518 } 519 520 /** 521 * This API is called when user explicitly selects a network. Currently, it is used in following 522 * cases: 523 * (1) User explicitly chooses to connect to a saved network. 524 * (2) User saves a network after adding a new network. 525 * (3) User saves a network after modifying a saved network. 526 * Following actions will be triggered: 527 * 1. If this network is disabled, we need re-enable it again. 528 * 2. This network is favored over all the other networks visible in latest network 529 * selection procedure. 530 * 531 * @param netId ID for the network chosen by the user 532 * @return true -- There is change made to connection choice of any saved network. 533 * false -- There is no change made to connection choice of any saved network. 534 */ setUserConnectChoice(int netId)535 public boolean setUserConnectChoice(int netId) { 536 localLog("userSelectNetwork: network ID=" + netId); 537 WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId); 538 539 if (selected == null || selected.SSID == null) { 540 localLog("userSelectNetwork: Invalid configuration with nid=" + netId); 541 return false; 542 } 543 544 // Enable the network if it is disabled. 545 if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { 546 mWifiConfigManager.updateNetworkSelectionStatus(netId, 547 WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE); 548 } 549 return setLegacyUserConnectChoice(selected); 550 } 551 552 /** 553 * This maintains the legacy user connect choice state in the config store 554 */ setLegacyUserConnectChoice(@onNull final WifiConfiguration selected)555 private boolean setLegacyUserConnectChoice(@NonNull final WifiConfiguration selected) { 556 boolean change = false; 557 String key = selected.getKey(); 558 List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); 559 560 for (WifiConfiguration network : configuredNetworks) { 561 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 562 if (network.networkId == selected.networkId) { 563 if (status.getConnectChoice() != null) { 564 localLog("Remove user selection preference of " + status.getConnectChoice() 565 + " from " + network.SSID + " : " + network.networkId); 566 mWifiConfigManager.clearNetworkConnectChoice(network.networkId); 567 change = true; 568 } 569 continue; 570 } 571 572 if (status.getSeenInLastQualifiedNetworkSelection() 573 && !key.equals(status.getConnectChoice())) { 574 localLog("Add key: " + key + " to " 575 + toNetworkString(network)); 576 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key); 577 change = true; 578 } 579 } 580 581 return change; 582 } 583 584 585 /** 586 * Iterate thru the list of configured networks (includes all saved network configurations + 587 * any ephemeral network configurations created for passpoint networks, suggestions, carrier 588 * networks, etc) and do the following: 589 * a) Try to re-enable any temporarily enabled networks (if the blacklist duration has expired). 590 * b) Clear the {@link WifiConfiguration.NetworkSelectionStatus#getCandidate()} field for all 591 * of them to identify networks that are present in the current scan result. 592 * c) Log any disabled networks. 593 */ updateConfiguredNetworks()594 private void updateConfiguredNetworks() { 595 List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); 596 if (configuredNetworks.size() == 0) { 597 localLog("No configured networks."); 598 return; 599 } 600 601 StringBuffer sbuf = new StringBuffer(); 602 for (WifiConfiguration network : configuredNetworks) { 603 // If a configuration is temporarily disabled, re-enable it before trying 604 // to connect to it. 605 mWifiConfigManager.tryEnableNetwork(network.networkId); 606 // Clear the cached candidate, score and seen. 607 mWifiConfigManager.clearNetworkCandidateScanResult(network.networkId); 608 609 // Log disabled network. 610 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 611 if (!status.isNetworkEnabled()) { 612 sbuf.append(" ").append(toNetworkString(network)).append(" "); 613 for (int index = WifiConfiguration.NetworkSelectionStatus 614 .NETWORK_SELECTION_DISABLED_STARTING_INDEX; 615 index < WifiConfiguration.NetworkSelectionStatus 616 .NETWORK_SELECTION_DISABLED_MAX; 617 index++) { 618 int count = status.getDisableReasonCounter(index); 619 // Here we log the reason as long as its count is greater than zero. The 620 // network may not be disabled because of this particular reason. Logging 621 // this information anyway to help understand what happened to the network. 622 if (count > 0) { 623 sbuf.append("reason=") 624 .append(WifiConfiguration.NetworkSelectionStatus 625 .getNetworkSelectionDisableReasonString(index)) 626 .append(", count=").append(count).append("; "); 627 } 628 } 629 sbuf.append("\n"); 630 } 631 } 632 633 if (sbuf.length() > 0) { 634 localLog("Disabled configured networks:"); 635 localLog(sbuf.toString()); 636 } 637 } 638 639 /** 640 * Overrides the {@code candidate} chosen by the {@link #mNominators} with the user chosen 641 * {@link WifiConfiguration} if one exists. 642 * 643 * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise 644 */ overrideCandidateWithUserConnectChoice( @onNull WifiConfiguration candidate)645 private WifiConfiguration overrideCandidateWithUserConnectChoice( 646 @NonNull WifiConfiguration candidate) { 647 WifiConfiguration tempConfig = Preconditions.checkNotNull(candidate); 648 WifiConfiguration originalCandidate = candidate; 649 ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate(); 650 651 while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) { 652 String key = tempConfig.getNetworkSelectionStatus().getConnectChoice(); 653 tempConfig = mWifiConfigManager.getConfiguredNetwork(key); 654 655 if (tempConfig != null) { 656 WifiConfiguration.NetworkSelectionStatus tempStatus = 657 tempConfig.getNetworkSelectionStatus(); 658 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) { 659 scanResultCandidate = tempStatus.getCandidate(); 660 candidate = tempConfig; 661 } 662 } else { 663 localLog("Connect choice: " + key + " has no corresponding saved config."); 664 break; 665 } 666 } 667 668 if (candidate != originalCandidate) { 669 localLog("After user selection adjustment, the final candidate is:" 670 + WifiNetworkSelector.toNetworkString(candidate) + " : " 671 + scanResultCandidate.BSSID); 672 mWifiMetrics.setNominatorForNetwork(candidate.networkId, 673 WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED_USER_CONNECT_CHOICE); 674 } 675 return candidate; 676 } 677 678 679 /** 680 * Indicates whether we have ever seen the network to be metered since wifi was enabled. 681 * 682 * This is sticky to prevent continuous flip-flopping between networks, when the metered 683 * status is learned after association. 684 */ isEverMetered(@onNull WifiConfiguration config, @Nullable WifiInfo info, @NonNull ScanDetail scanDetail)685 private boolean isEverMetered(@NonNull WifiConfiguration config, @Nullable WifiInfo info, 686 @NonNull ScanDetail scanDetail) { 687 // If info does not match config, don't use it. 688 if (info != null && info.getNetworkId() != config.networkId) info = null; 689 boolean metered = WifiConfiguration.isMetered(config, info); 690 NetworkDetail networkDetail = scanDetail.getNetworkDetail(); 691 if (networkDetail != null 692 && networkDetail.getAnt() 693 == NetworkDetail.Ant.ChargeablePublic) { 694 metered = true; 695 } 696 mWifiMetrics.addMeteredStat(config, metered); 697 if (config.meteredOverride != WifiConfiguration.METERED_OVERRIDE_NONE) { 698 // User override is in effect; we should trust it 699 if (mKnownMeteredNetworkIds.remove(config.networkId)) { 700 localLog("KnownMeteredNetworkIds = " + mKnownMeteredNetworkIds); 701 } 702 metered = config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED; 703 } else if (mKnownMeteredNetworkIds.contains(config.networkId)) { 704 // Use the saved information 705 metered = true; 706 } else if (metered) { 707 // Update the saved information 708 mKnownMeteredNetworkIds.add(config.networkId); 709 localLog("KnownMeteredNetworkIds = " + mKnownMeteredNetworkIds); 710 } 711 return metered; 712 } 713 714 /** 715 * Returns the set of known metered network ids (for tests. dumpsys, and metrics). 716 */ getKnownMeteredNetworkIds()717 public Set<Integer> getKnownMeteredNetworkIds() { 718 return new ArraySet<>(mKnownMeteredNetworkIds); 719 } 720 721 private final ArraySet<Integer> mKnownMeteredNetworkIds = new ArraySet<>(); 722 723 724 /** 725 * Cleans up state that should go away when wifi is disabled. 726 */ resetOnDisable()727 public void resetOnDisable() { 728 mWifiConfigManager.clearLastSelectedNetwork(); 729 mKnownMeteredNetworkIds.clear(); 730 } 731 732 /** 733 * Returns the list of Candidates from networks in range. 734 * 735 * @param scanDetails List of ScanDetail for all the APs in range 736 * @param bssidBlacklist Blacklisted BSSIDs 737 * @param wifiInfo Currently connected network 738 * @param connected True if the device is connected 739 * @param disconnected True if the device is disconnected 740 * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection 741 * @return list of valid Candidate(s) 742 */ getCandidatesFromScan( List<ScanDetail> scanDetails, Set<String> bssidBlacklist, WifiInfo wifiInfo, boolean connected, boolean disconnected, boolean untrustedNetworkAllowed)743 public List<WifiCandidates.Candidate> getCandidatesFromScan( 744 List<ScanDetail> scanDetails, Set<String> bssidBlacklist, WifiInfo wifiInfo, 745 boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) { 746 mFilteredNetworks.clear(); 747 mConnectableNetworks.clear(); 748 if (scanDetails.size() == 0) { 749 localLog("Empty connectivity scan result"); 750 return null; 751 } 752 753 WifiConfiguration currentNetwork = 754 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 755 756 // Always get the current BSSID from WifiInfo in case that firmware initiated 757 // roaming happened. 758 String currentBssid = wifiInfo.getBSSID(); 759 760 // Update the scan detail cache at the start, even if we skip network selection 761 updateScanDetailCache(scanDetails); 762 763 // Shall we start network selection at all? 764 if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { 765 return null; 766 } 767 768 // Update all configured networks before initiating network selection. 769 updateConfiguredNetworks(); 770 771 // Update the registered network nominators. 772 for (NetworkNominator registeredNominator : mNominators) { 773 registeredNominator.update(scanDetails); 774 } 775 776 // Filter out unwanted networks. 777 mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist, 778 connected && wifiInfo.getScore() >= WIFI_POOR_SCORE, currentBssid); 779 if (mFilteredNetworks.size() == 0) { 780 return null; 781 } 782 783 WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard, mContext); 784 if (currentNetwork != null) { 785 wifiCandidates.setCurrent(currentNetwork.networkId, currentBssid); 786 // We always want the current network to be a candidate so that it can participate. 787 // It may also get re-added by a nominator, in which case this fallback 788 // will be replaced. 789 MacAddress bssid = MacAddress.fromString(currentBssid); 790 WifiCandidates.Key key = new WifiCandidates.Key( 791 ScanResultMatchInfo.fromWifiConfiguration(currentNetwork), 792 bssid, currentNetwork.networkId); 793 wifiCandidates.add(key, currentNetwork, 794 NetworkNominator.NOMINATOR_ID_CURRENT, 795 wifiInfo.getRssi(), 796 wifiInfo.getFrequency(), 797 calculateLastSelectionWeight(currentNetwork.networkId), 798 WifiConfiguration.isMetered(currentNetwork, wifiInfo), 799 isFromCarrierOrPrivilegedApp(currentNetwork), 800 0 /* Mbps */); 801 } 802 for (NetworkNominator registeredNominator : mNominators) { 803 localLog("About to run " + registeredNominator.getName() + " :"); 804 registeredNominator.nominateNetworks( 805 new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected, 806 untrustedNetworkAllowed, 807 (scanDetail, config) -> { 808 WifiCandidates.Key key = wifiCandidates.keyFromScanDetailAndConfig( 809 scanDetail, config); 810 if (key != null) { 811 boolean metered = isEverMetered(config, wifiInfo, scanDetail); 812 // TODO(b/151981920) Saved passpoint candidates are marked ephemeral 813 boolean added = wifiCandidates.add(key, config, 814 registeredNominator.getId(), 815 scanDetail.getScanResult().level, 816 scanDetail.getScanResult().frequency, 817 calculateLastSelectionWeight(config.networkId), 818 metered, 819 isFromCarrierOrPrivilegedApp(config), 820 predictThroughput(scanDetail)); 821 if (added) { 822 mConnectableNetworks.add(Pair.create(scanDetail, config)); 823 mWifiConfigManager.updateScanDetailForNetwork( 824 config.networkId, scanDetail); 825 mWifiMetrics.setNominatorForNetwork(config.networkId, 826 toProtoNominatorId(registeredNominator.getId())); 827 } 828 } 829 }); 830 } 831 if (mConnectableNetworks.size() != wifiCandidates.size()) { 832 localLog("Connectable: " + mConnectableNetworks.size() 833 + " Candidates: " + wifiCandidates.size()); 834 } 835 return wifiCandidates.getCandidates(); 836 } 837 838 /** 839 * Using the registered Scorers, choose the best network from the list of Candidate(s). 840 * The ScanDetailCache is also updated here. 841 * @param candidates - Candidates to perferm network selection on. 842 * @return WifiConfiguration - the selected network, or null. 843 */ 844 @NonNull selectNetwork(List<WifiCandidates.Candidate> candidates)845 public WifiConfiguration selectNetwork(List<WifiCandidates.Candidate> candidates) { 846 if (candidates == null || candidates.size() == 0) { 847 return null; 848 } 849 WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard, mContext, candidates); 850 final WifiCandidates.CandidateScorer activeScorer = getActiveCandidateScorer(); 851 // Update the NetworkSelectionStatus in the configs for the current candidates 852 // This is needed for the legacy user connect choice, at least 853 Collection<Collection<WifiCandidates.Candidate>> groupedCandidates = 854 wifiCandidates.getGroupedCandidates(); 855 for (Collection<WifiCandidates.Candidate> group : groupedCandidates) { 856 WifiCandidates.ScoredCandidate choice = activeScorer.scoreCandidates(group); 857 if (choice == null) continue; 858 ScanDetail scanDetail = getScanDetailForCandidateKey(choice.candidateKey); 859 if (scanDetail == null) continue; 860 mWifiConfigManager.setNetworkCandidateScanResult(choice.candidateKey.networkId, 861 scanDetail.getScanResult(), 0); 862 } 863 864 for (Collection<WifiCandidates.Candidate> group : groupedCandidates) { 865 for (WifiCandidates.Candidate candidate : group.stream() 866 .sorted((a, b) -> (b.getScanRssi() - a.getScanRssi())) // decreasing rssi 867 .collect(Collectors.toList())) { 868 localLog(candidate.toString()); 869 } 870 } 871 872 ArrayMap<Integer, Integer> experimentNetworkSelections = new ArrayMap<>(); // for metrics 873 874 int selectedNetworkId = WifiConfiguration.INVALID_NETWORK_ID; 875 876 // Run all the CandidateScorers 877 boolean legacyOverrideWanted = true; 878 for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) { 879 WifiCandidates.ScoredCandidate choice; 880 try { 881 choice = wifiCandidates.choose(candidateScorer); 882 } catch (RuntimeException e) { 883 Log.wtf(TAG, "Exception running a CandidateScorer", e); 884 continue; 885 } 886 int networkId = choice.candidateKey == null 887 ? WifiConfiguration.INVALID_NETWORK_ID 888 : choice.candidateKey.networkId; 889 String chooses = " would choose "; 890 if (candidateScorer == activeScorer) { 891 chooses = " chooses "; 892 legacyOverrideWanted = choice.userConnectChoiceOverride; 893 selectedNetworkId = networkId; 894 updateChosenPasspointNetwork(choice); 895 } 896 String id = candidateScorer.getIdentifier(); 897 int expid = experimentIdFromIdentifier(id); 898 localLog(id + chooses + networkId 899 + " score " + choice.value + "+/-" + choice.err 900 + " expid " + expid); 901 experimentNetworkSelections.put(expid, networkId); 902 } 903 904 // Update metrics about differences in the selections made by various methods 905 final int activeExperimentId = experimentIdFromIdentifier(activeScorer.getIdentifier()); 906 for (Map.Entry<Integer, Integer> entry : 907 experimentNetworkSelections.entrySet()) { 908 int experimentId = entry.getKey(); 909 if (experimentId == activeExperimentId) continue; 910 int thisSelectedNetworkId = entry.getValue(); 911 mWifiMetrics.logNetworkSelectionDecision(experimentId, activeExperimentId, 912 selectedNetworkId == thisSelectedNetworkId, 913 groupedCandidates.size()); 914 } 915 916 // Get a fresh copy of WifiConfiguration reflecting any scan result updates 917 WifiConfiguration selectedNetwork = 918 mWifiConfigManager.getConfiguredNetwork(selectedNetworkId); 919 if (selectedNetwork != null && legacyOverrideWanted) { 920 selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork); 921 } 922 if (selectedNetwork != null) { 923 mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis(); 924 } 925 return selectedNetwork; 926 } 927 928 /** 929 * Returns the ScanDetail given the candidate key, using the saved list of connectible networks. 930 */ getScanDetailForCandidateKey(WifiCandidates.Key candidateKey)931 private ScanDetail getScanDetailForCandidateKey(WifiCandidates.Key candidateKey) { 932 if (candidateKey == null) return null; 933 String bssid = candidateKey.bssid.toString(); 934 for (Pair<ScanDetail, WifiConfiguration> pair : mConnectableNetworks) { 935 if (candidateKey.networkId == pair.second.networkId 936 && bssid.equals(pair.first.getBSSIDString())) { 937 return pair.first; 938 } 939 } 940 return null; 941 } 942 updateChosenPasspointNetwork(WifiCandidates.ScoredCandidate choice)943 private void updateChosenPasspointNetwork(WifiCandidates.ScoredCandidate choice) { 944 if (choice.candidateKey == null) { 945 return; 946 } 947 WifiConfiguration config = 948 mWifiConfigManager.getConfiguredNetwork(choice.candidateKey.networkId); 949 if (config == null) { 950 return; 951 } 952 if (config.isPasspoint()) { 953 config.SSID = choice.candidateKey.matchInfo.networkSsid; 954 mWifiConfigManager.addOrUpdateNetwork(config, config.creatorUid, config.creatorName); 955 } 956 } 957 updateScanDetailCache(List<ScanDetail> scanDetails)958 private void updateScanDetailCache(List<ScanDetail> scanDetails) { 959 for (ScanDetail scanDetail : scanDetails) { 960 mWifiConfigManager.updateScanDetailCacheFromScanDetail(scanDetail); 961 } 962 } 963 toProtoNominatorId(@etworkNominator.NominatorId int nominatorId)964 private static int toProtoNominatorId(@NetworkNominator.NominatorId int nominatorId) { 965 switch (nominatorId) { 966 case NetworkNominator.NOMINATOR_ID_SAVED: 967 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED; 968 case NetworkNominator.NOMINATOR_ID_SUGGESTION: 969 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SUGGESTION; 970 case NetworkNominator.NOMINATOR_ID_SCORED: 971 return WifiMetricsProto.ConnectionEvent.NOMINATOR_EXTERNAL_SCORED; 972 case NetworkNominator.NOMINATOR_ID_CURRENT: 973 Log.e(TAG, "Unexpected NOMINATOR_ID_CURRENT", new RuntimeException()); 974 return WifiMetricsProto.ConnectionEvent.NOMINATOR_UNKNOWN; 975 default: 976 Log.e(TAG, "UnrecognizedNominatorId" + nominatorId); 977 return WifiMetricsProto.ConnectionEvent.NOMINATOR_UNKNOWN; 978 } 979 } 980 calculateLastSelectionWeight(int networkId)981 private double calculateLastSelectionWeight(int networkId) { 982 if (networkId != mWifiConfigManager.getLastSelectedNetwork()) return 0.0; 983 double timeDifference = mClock.getElapsedSinceBootMillis() 984 - mWifiConfigManager.getLastSelectedTimeStamp(); 985 long millis = TimeUnit.MINUTES.toMillis(mScoringParams.getLastSelectionMinutes()); 986 if (timeDifference >= millis) return 0.0; 987 double unclipped = 1.0 - (timeDifference / millis); 988 return Math.min(Math.max(unclipped, 0.0), 1.0); 989 } 990 getActiveCandidateScorer()991 private WifiCandidates.CandidateScorer getActiveCandidateScorer() { 992 WifiCandidates.CandidateScorer ans = mCandidateScorers.get(PRESET_CANDIDATE_SCORER_NAME); 993 int overrideExperimentId = mScoringParams.getExperimentIdentifier(); 994 if (overrideExperimentId >= MIN_SCORER_EXP_ID) { 995 for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) { 996 int expId = experimentIdFromIdentifier(candidateScorer.getIdentifier()); 997 if (expId == overrideExperimentId) { 998 ans = candidateScorer; 999 break; 1000 } 1001 } 1002 } 1003 if (ans == null && PRESET_CANDIDATE_SCORER_NAME != null) { 1004 Log.wtf(TAG, PRESET_CANDIDATE_SCORER_NAME + " is not registered!"); 1005 } 1006 mWifiMetrics.setNetworkSelectorExperimentId(ans == null 1007 ? LEGACY_CANDIDATE_SCORER_EXP_ID 1008 : experimentIdFromIdentifier(ans.getIdentifier())); 1009 return ans; 1010 } 1011 predictThroughput(@onNull ScanDetail scanDetail)1012 private int predictThroughput(@NonNull ScanDetail scanDetail) { 1013 if (scanDetail.getScanResult() == null || scanDetail.getNetworkDetail() == null) { 1014 return 0; 1015 } 1016 int channelUtilizationLinkLayerStats = BssLoad.INVALID; 1017 if (mWifiChannelUtilization != null) { 1018 channelUtilizationLinkLayerStats = 1019 mWifiChannelUtilization.getUtilizationRatio( 1020 scanDetail.getScanResult().frequency); 1021 } 1022 return mThroughputPredictor.predictThroughput( 1023 mWifiNative.getDeviceWiphyCapabilities(mWifiNative.getClientInterfaceName()), 1024 scanDetail.getScanResult().getWifiStandard(), 1025 scanDetail.getScanResult().channelWidth, 1026 scanDetail.getScanResult().level, 1027 scanDetail.getScanResult().frequency, 1028 scanDetail.getNetworkDetail().getMaxNumberSpatialStreams(), 1029 scanDetail.getNetworkDetail().getChannelUtilization(), 1030 channelUtilizationLinkLayerStats, 1031 mIsBluetoothConnected); 1032 } 1033 1034 /** 1035 * Register a network nominator 1036 * 1037 * @param nominator the network nominator to be registered 1038 */ registerNetworkNominator(@onNull NetworkNominator nominator)1039 public void registerNetworkNominator(@NonNull NetworkNominator nominator) { 1040 mNominators.add(Preconditions.checkNotNull(nominator)); 1041 } 1042 1043 /** 1044 * Register a candidate scorer. 1045 * 1046 * Replaces any existing scorer having the same identifier. 1047 */ registerCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)1048 public void registerCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) { 1049 String name = Preconditions.checkNotNull(candidateScorer).getIdentifier(); 1050 if (name != null) { 1051 mCandidateScorers.put(name, candidateScorer); 1052 } 1053 } 1054 1055 /** 1056 * Unregister a candidate scorer. 1057 */ unregisterCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)1058 public void unregisterCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) { 1059 String name = Preconditions.checkNotNull(candidateScorer).getIdentifier(); 1060 if (name != null) { 1061 mCandidateScorers.remove(name); 1062 } 1063 } 1064 isFromCarrierOrPrivilegedApp(WifiConfiguration config)1065 private static boolean isFromCarrierOrPrivilegedApp(WifiConfiguration config) { 1066 if (config.fromWifiNetworkSuggestion 1067 && config.carrierId != TelephonyManager.UNKNOWN_CARRIER_ID) { 1068 // Privileged carrier suggestion 1069 return true; 1070 } 1071 if (config.isEphemeral() 1072 && !config.fromWifiNetworkSpecifier 1073 && !config.fromWifiNetworkSuggestion) { 1074 // From ScoredNetworkNominator 1075 return true; 1076 } 1077 return false; 1078 } 1079 1080 /** 1081 * Derives a numeric experiment identifier from a CandidateScorer's identifier. 1082 * 1083 * @returns a positive number that starts with the decimal digits ID_PREFIX 1084 */ experimentIdFromIdentifier(String id)1085 public static int experimentIdFromIdentifier(String id) { 1086 final int digits = (int) (((long) id.hashCode()) & Integer.MAX_VALUE) % ID_SUFFIX_MOD; 1087 return ID_PREFIX * ID_SUFFIX_MOD + digits; 1088 } 1089 1090 private static final int ID_SUFFIX_MOD = 1_000_000; 1091 private static final int ID_PREFIX = 42; 1092 private static final int MIN_SCORER_EXP_ID = ID_PREFIX * ID_SUFFIX_MOD; 1093 1094 /** 1095 * Set Wifi channel utilization calculated from link layer stats 1096 */ setWifiChannelUtilization(WifiChannelUtilization wifiChannelUtilization)1097 public void setWifiChannelUtilization(WifiChannelUtilization wifiChannelUtilization) { 1098 mWifiChannelUtilization = wifiChannelUtilization; 1099 } 1100 1101 /** 1102 * Set whether bluetooth is in the connected state 1103 */ setBluetoothConnected(boolean isBlueToothConnected)1104 public void setBluetoothConnected(boolean isBlueToothConnected) { 1105 mIsBluetoothConnected = isBlueToothConnected; 1106 } 1107 WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, WifiConfigManager configManager, Clock clock, LocalLog localLog, WifiMetrics wifiMetrics, WifiNative wifiNative, ThroughputPredictor throughputPredictor)1108 WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, 1109 WifiConfigManager configManager, Clock clock, LocalLog localLog, 1110 WifiMetrics wifiMetrics, WifiNative wifiNative, 1111 ThroughputPredictor throughputPredictor) { 1112 mContext = context; 1113 mWifiConfigManager = configManager; 1114 mClock = clock; 1115 mWifiScoreCard = wifiScoreCard; 1116 mScoringParams = scoringParams; 1117 mLocalLog = localLog; 1118 mWifiMetrics = wifiMetrics; 1119 mWifiNative = wifiNative; 1120 mThroughputPredictor = throughputPredictor; 1121 } 1122 } 1123