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.NetworkKey; 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.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.LocalLog; 34 import android.util.Log; 35 import android.util.Pair; 36 37 import com.android.internal.R; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.Preconditions; 40 import com.android.server.wifi.nano.WifiMetricsProto; 41 import com.android.server.wifi.util.ScanResultUtil; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Map; 50 51 /** 52 * This class looks at all the connectivity scan results then 53 * selects a network for the phone to connect or roam to. 54 */ 55 public class WifiNetworkSelector { 56 private static final String TAG = "WifiNetworkSelector"; 57 58 private static final long INVALID_TIME_STAMP = Long.MIN_VALUE; 59 60 /** 61 * Minimum time gap between last successful network selection and a 62 * new selection attempt. 63 */ 64 @VisibleForTesting 65 public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000; 66 67 /** 68 * For this duration after user selected it, consider the current network as sufficient. 69 * 70 * This delays network selection during the time that connectivity service may be posting 71 * a dialog about a no-internet network. 72 */ 73 @VisibleForTesting 74 public static final int LAST_USER_SELECTION_SUFFICIENT_MS = 30_000; 75 76 /** 77 * Time that it takes for the boost given to the most recently user-selected 78 * network to decay to zero. 79 * 80 * In milliseconds. 81 */ 82 @VisibleForTesting 83 public static final int LAST_USER_SELECTION_DECAY_TO_ZERO_MS = 8 * 60 * 60 * 1000; 84 85 /** 86 * Connected score value used to decide whether a still-connected wifi should be treated 87 * as unconnected when filtering scan results. 88 */ 89 @VisibleForTesting 90 public static final int WIFI_POOR_SCORE = ConnectedScore.WIFI_TRANSITION_SCORE - 10; 91 92 /** 93 * The identifier string of the CandidateScorer to use (in the absence of overrides). 94 */ 95 public static final String PRESET_CANDIDATE_SCORER_NAME = "CompatibilityScorer"; 96 97 /** 98 * Experiment ID for the legacy scorer. 99 */ 100 public static final int LEGACY_CANDIDATE_SCORER_EXP_ID = 0; 101 102 private final WifiConfigManager mWifiConfigManager; 103 private final Clock mClock; 104 private final LocalLog mLocalLog; 105 private final WifiMetrics mWifiMetrics; 106 private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP; 107 // Buffer of filtered scan results (Scan results considered by network selection) & associated 108 // WifiConfiguration (if any). 109 private final List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks = 110 new ArrayList<>(); 111 private List<ScanDetail> mFilteredNetworks = new ArrayList<>(); 112 private final WifiScoreCard mWifiScoreCard; 113 private final ScoringParams mScoringParams; 114 private final int mStayOnNetworkMinimumTxRate; 115 private final int mStayOnNetworkMinimumRxRate; 116 private final boolean mEnableAutoJoinWhenAssociated; 117 private final WifiNative mWifiNative; 118 119 private final Map<String, WifiCandidates.CandidateScorer> mCandidateScorers = new ArrayMap<>(); 120 private boolean mIsEnhancedOpenSupportedInitialized = false; 121 private boolean mIsEnhancedOpenSupported; 122 123 /** 124 * WiFi Network Selector supports various categories of networks. Each category 125 * has an evaluator to choose the best WiFi network to connect to. Evaluators 126 * should be registered in order, by decreasing importance. 127 * Wifi Network Selector iterates through the registered scorers in registration order 128 * before making a final selection from among the candidates. 129 */ 130 131 /** 132 * Interface for WiFi Network Evaluator 133 * 134 * A network evaluator examines the scan results and recommends the 135 * best network in its category to connect to; it also reports the 136 * connectable candidates in its category for further consideration. 137 */ 138 public interface NetworkEvaluator { 139 /** Type of evaluators */ 140 int EVALUATOR_ID_SAVED = 0; 141 int EVALUATOR_ID_SUGGESTION = 1; 142 int EVALUATOR_ID_PASSPOINT = 2; 143 int EVALUATOR_ID_CARRIER = 3; 144 int EVALUATOR_ID_SCORED = 4; 145 146 @IntDef(prefix = { "EVALUATOR_ID_" }, value = { 147 EVALUATOR_ID_SAVED, 148 EVALUATOR_ID_SUGGESTION, 149 EVALUATOR_ID_PASSPOINT, 150 EVALUATOR_ID_CARRIER, 151 EVALUATOR_ID_SCORED}) 152 @Retention(RetentionPolicy.SOURCE) 153 public @interface EvaluatorId {} 154 155 /** 156 * Get the evaluator type. 157 */ getId()158 @EvaluatorId int getId(); 159 160 /** 161 * Get the evaluator name. 162 */ getName()163 String getName(); 164 165 /** 166 * Update the evaluator. 167 * 168 * Certain evaluators have to be updated with the new scan results. For example 169 * the ScoredNetworkEvaluator needs to refresh its Score Cache. 170 * 171 * @param scanDetails a list of scan details constructed from the scan results 172 */ update(List<ScanDetail> scanDetails)173 void update(List<ScanDetail> scanDetails); 174 175 /** 176 * Evaluate all the networks from the scan results. 177 * 178 * @param scanDetails a list of scan details constructed from the scan results 179 * @param currentNetwork configuration of the current connected network 180 * or null if disconnected 181 * @param currentBssid BSSID of the current connected network or null if 182 * disconnected 183 * @param connected a flag to indicate if ClientModeImpl is in connected 184 * state 185 * @param untrustedNetworkAllowed a flag to indicate if untrusted networks like 186 * ephemeral networks are allowed 187 * @param onConnectableListener callback to record all of the connectable networks 188 * 189 * @return configuration of the chosen network; 190 * null if no network in this category is available. 191 */ 192 @Nullable evaluateNetworks(List<ScanDetail> scanDetails, WifiConfiguration currentNetwork, String currentBssid, boolean connected, boolean untrustedNetworkAllowed, OnConnectableListener onConnectableListener)193 WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails, 194 WifiConfiguration currentNetwork, String currentBssid, 195 boolean connected, boolean untrustedNetworkAllowed, 196 OnConnectableListener onConnectableListener); 197 198 /** 199 * Callback for recording connectable candidates 200 */ 201 public interface OnConnectableListener { 202 /** 203 * Notes that an access point is an eligible connection candidate 204 * 205 * @param scanDetail describes the specific access point 206 * @param config is the WifiConfiguration for the network 207 * @param score is the score assigned by the evaluator 208 */ onConnectable(ScanDetail scanDetail, WifiConfiguration config, int score)209 void onConnectable(ScanDetail scanDetail, WifiConfiguration config, int score); 210 } 211 } 212 213 private final List<NetworkEvaluator> mEvaluators = new ArrayList<>(3); 214 215 // A helper to log debugging information in the local log buffer, which can 216 // be retrieved in bugreport. localLog(String log)217 private void localLog(String log) { 218 mLocalLog.log(log); 219 } 220 isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails)221 private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails) { 222 // Currently connected? 223 if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) { 224 localLog("No current connected network."); 225 return false; 226 } else { 227 localLog("Current connected network: " + wifiInfo.getSSID() 228 + " , ID: " + wifiInfo.getNetworkId()); 229 } 230 231 int currentRssi = wifiInfo.getRssi(); 232 boolean hasQualifiedRssi = currentRssi 233 > mScoringParams.getSufficientRssi(wifiInfo.getFrequency()); 234 boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate) 235 || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate); 236 if (hasQualifiedRssi && hasActiveStream) { 237 localLog("Stay on current network because of good RSSI and ongoing traffic"); 238 return true; 239 } 240 WifiConfiguration network = 241 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 242 243 if (network == null) { 244 localLog("Current network was removed."); 245 return false; 246 } 247 248 if (mWifiConfigManager.getLastSelectedNetwork() == network.networkId 249 && (mClock.getElapsedSinceBootMillis() 250 - mWifiConfigManager.getLastSelectedTimeStamp()) 251 <= LAST_USER_SELECTION_SUFFICIENT_MS) { 252 localLog("Current network is recently user-selected."); 253 return true; 254 } 255 256 // OSU (Online Sign Up) network for Passpoint Release 2 is sufficient network. 257 if (network.osu) { 258 return true; 259 } 260 261 // Ephemeral network is not qualified. 262 if (wifiInfo.isEphemeral()) { 263 localLog("Current network is an ephemeral one."); 264 return false; 265 } 266 267 if (wifiInfo.is24GHz()) { 268 // 2.4GHz networks is not qualified whenever 5GHz is available 269 if (is5GHzNetworkAvailable(scanDetails)) { 270 localLog("Current network is 2.4GHz. 5GHz networks available."); 271 return false; 272 } 273 } 274 if (!hasQualifiedRssi) { 275 localLog("Current network RSSI[" + currentRssi + "]-acceptable but not qualified."); 276 return false; 277 } 278 279 // Open network is not qualified. 280 if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) { 281 localLog("Current network is a open one."); 282 return false; 283 } 284 285 // Network with no internet access reports is not qualified. 286 if (network.numNoInternetAccessReports > 0 && !network.noInternetAccessExpected) { 287 localLog("Current network has [" + network.numNoInternetAccessReports 288 + "] no-internet access reports."); 289 return false; 290 } 291 return true; 292 } 293 294 // Determine whether there are any 5GHz networks in the scan result is5GHzNetworkAvailable(List<ScanDetail> scanDetails)295 private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) { 296 for (ScanDetail detail : scanDetails) { 297 ScanResult result = detail.getScanResult(); 298 if (result.is5GHz()) return true; 299 } 300 return false; 301 } 302 isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, boolean connected, boolean disconnected)303 private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo, 304 boolean connected, boolean disconnected) { 305 if (scanDetails.size() == 0) { 306 localLog("Empty connectivity scan results. Skip network selection."); 307 return false; 308 } 309 310 if (connected) { 311 // Is roaming allowed? 312 if (!mEnableAutoJoinWhenAssociated) { 313 localLog("Switching networks in connected state is not allowed." 314 + " Skip network selection."); 315 return false; 316 } 317 318 // Has it been at least the minimum interval since last network selection? 319 if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) { 320 long gap = mClock.getElapsedSinceBootMillis() 321 - mLastNetworkSelectionTimeStamp; 322 if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) { 323 localLog("Too short since last network selection: " + gap + " ms." 324 + " Skip network selection."); 325 return false; 326 } 327 } 328 329 if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) { 330 localLog("Current connected network already sufficient. Skip network selection."); 331 return false; 332 } else { 333 localLog("Current connected network is not sufficient."); 334 return true; 335 } 336 } else if (disconnected) { 337 return true; 338 } else { 339 // No network selection if ClientModeImpl is in a state other than 340 // CONNECTED or DISCONNECTED. 341 localLog("ClientModeImpl is in neither CONNECTED nor DISCONNECTED state." 342 + " Skip network selection."); 343 return false; 344 } 345 } 346 347 /** 348 * Format the given ScanResult as a scan ID for logging. 349 */ toScanId(@ullable ScanResult scanResult)350 public static String toScanId(@Nullable ScanResult scanResult) { 351 return scanResult == null ? "NULL" 352 : String.format("%s:%s", scanResult.SSID, scanResult.BSSID); 353 } 354 355 /** 356 * Format the given WifiConfiguration as a SSID:netId string 357 */ toNetworkString(WifiConfiguration network)358 public static String toNetworkString(WifiConfiguration network) { 359 if (network == null) { 360 return null; 361 } 362 363 return (network.SSID + ":" + network.networkId); 364 } 365 366 /** 367 * Compares ScanResult level against the minimum threshold for its band, returns true if lower 368 */ isSignalTooWeak(ScanResult scanResult)369 public boolean isSignalTooWeak(ScanResult scanResult) { 370 return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency)); 371 } 372 filterScanResults(List<ScanDetail> scanDetails, HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid)373 private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails, 374 HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) { 375 ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>(); 376 List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>(); 377 StringBuffer noValidSsid = new StringBuffer(); 378 StringBuffer blacklistedBssid = new StringBuffer(); 379 StringBuffer lowRssi = new StringBuffer(); 380 boolean scanResultsHaveCurrentBssid = false; 381 382 for (ScanDetail scanDetail : scanDetails) { 383 ScanResult scanResult = scanDetail.getScanResult(); 384 385 if (TextUtils.isEmpty(scanResult.SSID)) { 386 noValidSsid.append(scanResult.BSSID).append(" / "); 387 continue; 388 } 389 390 // Check if the scan results contain the currently connected BSSID 391 if (scanResult.BSSID.equals(currentBssid)) { 392 scanResultsHaveCurrentBssid = true; 393 } 394 395 final String scanId = toScanId(scanResult); 396 397 if (bssidBlacklist.contains(scanResult.BSSID)) { 398 blacklistedBssid.append(scanId).append(" / "); 399 continue; 400 } 401 402 // Skip network with too weak signals. 403 if (isSignalTooWeak(scanResult)) { 404 lowRssi.append(scanId).append("(") 405 .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz") 406 .append(")").append(scanResult.level).append(" / "); 407 continue; 408 } 409 410 validScanDetails.add(scanDetail); 411 } 412 413 // WNS listens to all single scan results. Some scan requests may not include 414 // the channel of the currently connected network, so the currently connected 415 // network won't show up in the scan results. We don't act on these scan results 416 // to avoid aggressive network switching which might trigger disconnection. 417 if (isConnected && !scanResultsHaveCurrentBssid) { 418 localLog("Current connected BSSID " + currentBssid + " is not in the scan results." 419 + " Skip network selection."); 420 validScanDetails.clear(); 421 return validScanDetails; 422 } 423 424 if (noValidSsid.length() != 0) { 425 localLog("Networks filtered out due to invalid SSID: " + noValidSsid); 426 } 427 428 if (blacklistedBssid.length() != 0) { 429 localLog("Networks filtered out due to blacklist: " + blacklistedBssid); 430 } 431 432 if (lowRssi.length() != 0) { 433 localLog("Networks filtered out due to low signal strength: " + lowRssi); 434 } 435 436 return validScanDetails; 437 } 438 isEnhancedOpenSupported()439 private boolean isEnhancedOpenSupported() { 440 if (mIsEnhancedOpenSupportedInitialized) { 441 return mIsEnhancedOpenSupported; 442 } 443 444 mIsEnhancedOpenSupportedInitialized = true; 445 mIsEnhancedOpenSupported = (mWifiNative.getSupportedFeatureSet( 446 mWifiNative.getClientInterfaceName()) & WIFI_FEATURE_OWE) != 0; 447 return mIsEnhancedOpenSupported; 448 } 449 450 /** 451 * This returns a list of ScanDetails that were filtered in the process of network selection. 452 * The list is further filtered for only open unsaved networks. 453 * 454 * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS, 455 * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are 456 * no open unsaved networks, or when network selection has not been run. 457 */ getFilteredScanDetailsForOpenUnsavedNetworks()458 public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() { 459 List<ScanDetail> openUnsavedNetworks = new ArrayList<>(); 460 boolean enhancedOpenSupported = isEnhancedOpenSupported(); 461 for (ScanDetail scanDetail : mFilteredNetworks) { 462 ScanResult scanResult = scanDetail.getScanResult(); 463 464 if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) { 465 continue; 466 } 467 468 // Filter out Enhanced Open networks on devices that do not support it 469 if (ScanResultUtil.isScanResultForOweNetwork(scanResult) 470 && !enhancedOpenSupported) { 471 continue; 472 } 473 474 // Skip saved networks 475 if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) { 476 continue; 477 } 478 479 openUnsavedNetworks.add(scanDetail); 480 } 481 return openUnsavedNetworks; 482 } 483 484 /** 485 * This returns a list of ScanDetails that were filtered in the process of network selection. 486 * The list is further filtered for only carrier unsaved networks with EAP encryption. 487 * 488 * @param carrierConfig CarrierNetworkConfig used to filter carrier networks 489 * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS, 490 * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an 491 * empty list when there are no such networks, or when network selection has not been run. 492 */ getFilteredScanDetailsForCarrierUnsavedNetworks( CarrierNetworkConfig carrierConfig)493 public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks( 494 CarrierNetworkConfig carrierConfig) { 495 List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>(); 496 for (ScanDetail scanDetail : mFilteredNetworks) { 497 ScanResult scanResult = scanDetail.getScanResult(); 498 499 if (!ScanResultUtil.isScanResultForEapNetwork(scanResult) 500 || !carrierConfig.isCarrierNetwork(scanResult.SSID)) { 501 continue; 502 } 503 504 // Skip saved networks 505 if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) { 506 continue; 507 } 508 509 carrierUnsavedNetworks.add(scanDetail); 510 } 511 return carrierUnsavedNetworks; 512 } 513 514 /** 515 * @return the list of ScanDetails scored as potential candidates by the last run of 516 * selectNetwork, this will be empty if Network selector determined no selection was 517 * needed on last run. This includes scan details of sufficient signal strength, and 518 * had an associated WifiConfiguration. 519 */ getConnectableScanDetails()520 public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() { 521 return mConnectableNetworks; 522 } 523 524 /** 525 * This API is called when user explicitly selects a network. Currently, it is used in following 526 * cases: 527 * (1) User explicitly chooses to connect to a saved network. 528 * (2) User saves a network after adding a new network. 529 * (3) User saves a network after modifying a saved network. 530 * Following actions will be triggered: 531 * 1. If this network is disabled, we need re-enable it again. 532 * 2. This network is favored over all the other networks visible in latest network 533 * selection procedure. 534 * 535 * @param netId ID for the network chosen by the user 536 * @return true -- There is change made to connection choice of any saved network. 537 * false -- There is no change made to connection choice of any saved network. 538 */ setUserConnectChoice(int netId)539 public boolean setUserConnectChoice(int netId) { 540 localLog("userSelectNetwork: network ID=" + netId); 541 WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId); 542 543 if (selected == null || selected.SSID == null) { 544 localLog("userSelectNetwork: Invalid configuration with nid=" + netId); 545 return false; 546 } 547 548 // Enable the network if it is disabled. 549 if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) { 550 mWifiConfigManager.updateNetworkSelectionStatus(netId, 551 WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE); 552 } 553 return setLegacyUserConnectChoice(selected); 554 } 555 556 /** 557 * This maintains the legacy user connect choice state in the config store 558 */ setLegacyUserConnectChoice(@onNull final WifiConfiguration selected)559 private boolean setLegacyUserConnectChoice(@NonNull final WifiConfiguration selected) { 560 boolean change = false; 561 String key = selected.configKey(); 562 // This is only used for setting the connect choice timestamp for debugging purposes. 563 long currentTime = mClock.getWallClockMillis(); 564 List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); 565 566 for (WifiConfiguration network : configuredNetworks) { 567 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 568 if (network.networkId == selected.networkId) { 569 if (status.getConnectChoice() != null) { 570 localLog("Remove user selection preference of " + status.getConnectChoice() 571 + " Set Time: " + status.getConnectChoiceTimestamp() + " from " 572 + network.SSID + " : " + network.networkId); 573 mWifiConfigManager.clearNetworkConnectChoice(network.networkId); 574 change = true; 575 } 576 continue; 577 } 578 579 if (status.getSeenInLastQualifiedNetworkSelection() 580 && !key.equals(status.getConnectChoice())) { 581 localLog("Add key: " + key + " Set Time: " + currentTime + " to " 582 + toNetworkString(network)); 583 mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime); 584 change = true; 585 } 586 } 587 588 return change; 589 } 590 591 592 /** 593 * Iterate thru the list of configured networks (includes all saved network configurations + 594 * any ephemeral network configurations created for passpoint networks, suggestions, carrier 595 * networks, etc) and do the following: 596 * a) Try to re-enable any temporarily enabled networks (if the blacklist duration has expired). 597 * b) Clear the {@link WifiConfiguration.NetworkSelectionStatus#getCandidate()} field for all 598 * of them to identify networks that are present in the current scan result. 599 * c) Log any disabled networks. 600 */ updateConfiguredNetworks()601 private void updateConfiguredNetworks() { 602 List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); 603 if (configuredNetworks.size() == 0) { 604 localLog("No configured networks."); 605 return; 606 } 607 608 StringBuffer sbuf = new StringBuffer(); 609 for (WifiConfiguration network : configuredNetworks) { 610 // If a configuration is temporarily disabled, re-enable it before trying 611 // to connect to it. 612 mWifiConfigManager.tryEnableNetwork(network.networkId); 613 // Clear the cached candidate, score and seen. 614 mWifiConfigManager.clearNetworkCandidateScanResult(network.networkId); 615 616 // Log disabled network. 617 WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus(); 618 if (!status.isNetworkEnabled()) { 619 sbuf.append(" ").append(toNetworkString(network)).append(" "); 620 for (int index = WifiConfiguration.NetworkSelectionStatus 621 .NETWORK_SELECTION_DISABLED_STARTING_INDEX; 622 index < WifiConfiguration.NetworkSelectionStatus 623 .NETWORK_SELECTION_DISABLED_MAX; 624 index++) { 625 int count = status.getDisableReasonCounter(index); 626 // Here we log the reason as long as its count is greater than zero. The 627 // network may not be disabled because of this particular reason. Logging 628 // this information anyway to help understand what happened to the network. 629 if (count > 0) { 630 sbuf.append("reason=") 631 .append(WifiConfiguration.NetworkSelectionStatus 632 .getNetworkDisableReasonString(index)) 633 .append(", count=").append(count).append("; "); 634 } 635 } 636 sbuf.append("\n"); 637 } 638 } 639 640 if (sbuf.length() > 0) { 641 localLog("Disabled configured networks:"); 642 localLog(sbuf.toString()); 643 } 644 } 645 646 /** 647 * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen 648 * {@link WifiConfiguration} if one exists. 649 * 650 * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise 651 */ overrideCandidateWithUserConnectChoice( @onNull WifiConfiguration candidate)652 private WifiConfiguration overrideCandidateWithUserConnectChoice( 653 @NonNull WifiConfiguration candidate) { 654 WifiConfiguration tempConfig = Preconditions.checkNotNull(candidate); 655 WifiConfiguration originalCandidate = candidate; 656 ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate(); 657 658 while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) { 659 String key = tempConfig.getNetworkSelectionStatus().getConnectChoice(); 660 tempConfig = mWifiConfigManager.getConfiguredNetwork(key); 661 662 if (tempConfig != null) { 663 WifiConfiguration.NetworkSelectionStatus tempStatus = 664 tempConfig.getNetworkSelectionStatus(); 665 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) { 666 scanResultCandidate = tempStatus.getCandidate(); 667 candidate = tempConfig; 668 } 669 } else { 670 localLog("Connect choice: " + key + " has no corresponding saved config."); 671 break; 672 } 673 } 674 675 if (candidate != originalCandidate) { 676 localLog("After user selection adjustment, the final candidate is:" 677 + WifiNetworkSelector.toNetworkString(candidate) + " : " 678 + scanResultCandidate.BSSID); 679 mWifiMetrics.setNominatorForNetwork(candidate.networkId, 680 WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED_USER_CONNECT_CHOICE); 681 } 682 return candidate; 683 } 684 685 /** 686 * Select the best network from the ones in range. 687 * 688 * @param scanDetails List of ScanDetail for all the APs in range 689 * @param bssidBlacklist Blacklisted BSSIDs 690 * @param wifiInfo Currently connected network 691 * @param connected True if the device is connected 692 * @param disconnected True if the device is disconnected 693 * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection 694 * @return Configuration of the selected network, or Null if nothing 695 */ 696 @Nullable selectNetwork(List<ScanDetail> scanDetails, HashSet<String> bssidBlacklist, WifiInfo wifiInfo, boolean connected, boolean disconnected, boolean untrustedNetworkAllowed)697 public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, 698 HashSet<String> bssidBlacklist, WifiInfo wifiInfo, 699 boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) { 700 mFilteredNetworks.clear(); 701 mConnectableNetworks.clear(); 702 if (scanDetails.size() == 0) { 703 localLog("Empty connectivity scan result"); 704 return null; 705 } 706 707 WifiConfiguration currentNetwork = 708 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId()); 709 710 // Always get the current BSSID from WifiInfo in case that firmware initiated 711 // roaming happened. 712 String currentBssid = wifiInfo.getBSSID(); 713 714 // Shall we start network selection at all? 715 if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { 716 return null; 717 } 718 719 // Update all configured networks before initiating network selection. 720 updateConfiguredNetworks(); 721 722 // Update the registered network evaluators. 723 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 724 registeredEvaluator.update(scanDetails); 725 } 726 727 // Filter out unwanted networks. 728 mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist, 729 connected && wifiInfo.score >= WIFI_POOR_SCORE, currentBssid); 730 if (mFilteredNetworks.size() == 0) { 731 return null; 732 } 733 734 // Determine the weight for the last user selection 735 final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork(); 736 final double lastSelectionWeight = calculateLastSelectionWeight(); 737 final ArraySet<Integer> mNetworkIds = new ArraySet<>(); 738 739 // Go through the registered network evaluators in order 740 WifiConfiguration selectedNetwork = null; 741 WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard); 742 if (currentNetwork != null) { 743 wifiCandidates.setCurrent(currentNetwork.networkId, currentBssid); 744 } 745 for (NetworkEvaluator registeredEvaluator : mEvaluators) { 746 localLog("About to run " + registeredEvaluator.getName() + " :"); 747 WifiConfiguration choice = registeredEvaluator.evaluateNetworks( 748 new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected, 749 untrustedNetworkAllowed, 750 (scanDetail, config, score) -> { 751 if (config != null) { 752 mConnectableNetworks.add(Pair.create(scanDetail, config)); 753 mNetworkIds.add(config.networkId); 754 if (config.networkId == lastUserSelectedNetworkId) { 755 wifiCandidates.add(scanDetail, config, 756 registeredEvaluator.getId(), score, lastSelectionWeight); 757 } else { 758 wifiCandidates.add(scanDetail, config, 759 registeredEvaluator.getId(), score); 760 } 761 mWifiMetrics.setNominatorForNetwork(config.networkId, 762 evaluatorIdToNominatorId(registeredEvaluator.getId())); 763 } 764 }); 765 if (choice != null && !mNetworkIds.contains(choice.networkId)) { 766 Log.wtf(TAG, registeredEvaluator.getName() 767 + " failed to report choice with noConnectibleListener"); 768 } 769 if (selectedNetwork == null && choice != null) { 770 selectedNetwork = choice; // First one wins 771 localLog(registeredEvaluator.getName() + " selects " 772 + WifiNetworkSelector.toNetworkString(selectedNetwork)); 773 } 774 } 775 776 if (mConnectableNetworks.size() != wifiCandidates.size()) { 777 localLog("Connectable: " + mConnectableNetworks.size() 778 + " Candidates: " + wifiCandidates.size()); 779 } 780 781 // Update the NetworkSelectionStatus in the configs for the current candidates 782 // This is needed for the legacy user connect choice, at least 783 Collection<Collection<WifiCandidates.Candidate>> groupedCandidates = 784 wifiCandidates.getGroupedCandidates(); 785 for (Collection<WifiCandidates.Candidate> group: groupedCandidates) { 786 WifiCandidates.Candidate best = null; 787 for (WifiCandidates.Candidate candidate: group) { 788 // Of all the candidates with the same networkId, choose the 789 // one with the smallest evaluatorId, and break ties by 790 // picking the one with the highest score. 791 if (best == null 792 || candidate.getEvaluatorId() < best.getEvaluatorId() 793 || (candidate.getEvaluatorId() == best.getEvaluatorId() 794 && candidate.getEvaluatorScore() > best.getEvaluatorScore())) { 795 best = candidate; 796 } 797 } 798 if (best != null) { 799 ScanDetail scanDetail = best.getScanDetail(); 800 if (scanDetail != null) { 801 mWifiConfigManager.setNetworkCandidateScanResult(best.getNetworkConfigId(), 802 scanDetail.getScanResult(), best.getEvaluatorScore()); 803 } 804 } 805 } 806 807 ArrayMap<Integer, Integer> experimentNetworkSelections = new ArrayMap<>(); // for metrics 808 809 final int legacySelectedNetworkId = selectedNetwork == null 810 ? WifiConfiguration.INVALID_NETWORK_ID 811 : selectedNetwork.networkId; 812 813 int selectedNetworkId = legacySelectedNetworkId; 814 815 // Run all the CandidateScorers 816 boolean legacyOverrideWanted = true; 817 final WifiCandidates.CandidateScorer activeScorer = getActiveCandidateScorer(); 818 for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) { 819 WifiCandidates.ScoredCandidate choice; 820 try { 821 choice = wifiCandidates.choose(candidateScorer); 822 } catch (RuntimeException e) { 823 Log.wtf(TAG, "Exception running a CandidateScorer", e); 824 continue; 825 } 826 int networkId = choice.candidateKey == null 827 ? WifiConfiguration.INVALID_NETWORK_ID 828 : choice.candidateKey.networkId; 829 String chooses = " would choose "; 830 if (candidateScorer == activeScorer) { 831 chooses = " chooses "; 832 legacyOverrideWanted = candidateScorer.userConnectChoiceOverrideWanted(); 833 selectedNetworkId = networkId; 834 } 835 String id = candidateScorer.getIdentifier(); 836 int expid = experimentIdFromIdentifier(id); 837 localLog(id + chooses + networkId 838 + " score " + choice.value + "+/-" + choice.err 839 + " expid " + expid); 840 experimentNetworkSelections.put(expid, networkId); 841 } 842 843 // Update metrics about differences in the selections made by various methods 844 final int activeExperimentId = activeScorer == null ? LEGACY_CANDIDATE_SCORER_EXP_ID 845 : experimentIdFromIdentifier(activeScorer.getIdentifier()); 846 experimentNetworkSelections.put(LEGACY_CANDIDATE_SCORER_EXP_ID, legacySelectedNetworkId); 847 for (Map.Entry<Integer, Integer> entry : 848 experimentNetworkSelections.entrySet()) { 849 int experimentId = entry.getKey(); 850 if (experimentId == activeExperimentId) continue; 851 int thisSelectedNetworkId = entry.getValue(); 852 mWifiMetrics.logNetworkSelectionDecision(experimentId, activeExperimentId, 853 selectedNetworkId == thisSelectedNetworkId, 854 groupedCandidates.size()); 855 } 856 857 // Get a fresh copy of WifiConfiguration reflecting any scan result updates 858 selectedNetwork = mWifiConfigManager.getConfiguredNetwork(selectedNetworkId); 859 if (selectedNetwork != null && legacyOverrideWanted) { 860 selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork); 861 mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis(); 862 } 863 return selectedNetwork; 864 } 865 evaluatorIdToNominatorId(@etworkEvaluator.EvaluatorId int evaluatorId)866 private static int evaluatorIdToNominatorId(@NetworkEvaluator.EvaluatorId int evaluatorId) { 867 switch (evaluatorId) { 868 case NetworkEvaluator.EVALUATOR_ID_SAVED: 869 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED; 870 case NetworkEvaluator.EVALUATOR_ID_SUGGESTION: 871 return WifiMetricsProto.ConnectionEvent.NOMINATOR_SUGGESTION; 872 case NetworkEvaluator.EVALUATOR_ID_PASSPOINT: 873 return WifiMetricsProto.ConnectionEvent.NOMINATOR_PASSPOINT; 874 case NetworkEvaluator.EVALUATOR_ID_CARRIER: 875 return WifiMetricsProto.ConnectionEvent.NOMINATOR_CARRIER; 876 case NetworkEvaluator.EVALUATOR_ID_SCORED: 877 return WifiMetricsProto.ConnectionEvent.NOMINATOR_EXTERNAL_SCORED; 878 default: 879 Log.e(TAG, "UnrecognizedEvaluatorId" + evaluatorId); 880 return WifiMetricsProto.ConnectionEvent.NOMINATOR_UNKNOWN; 881 } 882 } 883 calculateLastSelectionWeight()884 private double calculateLastSelectionWeight() { 885 final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork(); 886 double lastSelectionWeight = 0.0; 887 if (lastUserSelectedNetworkId != WifiConfiguration.INVALID_NETWORK_ID) { 888 double timeDifference = mClock.getElapsedSinceBootMillis() 889 - mWifiConfigManager.getLastSelectedTimeStamp(); 890 double unclipped = 1.0 - (timeDifference / LAST_USER_SELECTION_DECAY_TO_ZERO_MS); 891 lastSelectionWeight = Math.min(Math.max(unclipped, 0.0), 1.0); 892 } 893 return lastSelectionWeight; 894 } 895 getActiveCandidateScorer()896 private WifiCandidates.CandidateScorer getActiveCandidateScorer() { 897 WifiCandidates.CandidateScorer ans = mCandidateScorers.get(PRESET_CANDIDATE_SCORER_NAME); 898 int overrideExperimentId = mScoringParams.getExperimentIdentifier(); 899 if (overrideExperimentId >= MIN_SCORER_EXP_ID) { 900 for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) { 901 int expId = experimentIdFromIdentifier(candidateScorer.getIdentifier()); 902 if (expId == overrideExperimentId) { 903 ans = candidateScorer; 904 break; 905 } 906 } 907 } 908 if (ans == null && PRESET_CANDIDATE_SCORER_NAME != null) { 909 Log.wtf(TAG, PRESET_CANDIDATE_SCORER_NAME + " is not registered!"); 910 } 911 mWifiMetrics.setNetworkSelectorExperimentId(ans == null 912 ? LEGACY_CANDIDATE_SCORER_EXP_ID 913 : experimentIdFromIdentifier(ans.getIdentifier())); 914 return ans; 915 } 916 917 /** 918 * Register a network evaluator 919 * 920 * @param evaluator the network evaluator to be registered 921 * 922 */ registerNetworkEvaluator(@onNull NetworkEvaluator evaluator)923 public void registerNetworkEvaluator(@NonNull NetworkEvaluator evaluator) { 924 mEvaluators.add(Preconditions.checkNotNull(evaluator)); 925 } 926 927 /** 928 * Register a candidate scorer. 929 * 930 * Replaces any existing scorer having the same identifier. 931 */ registerCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)932 public void registerCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) { 933 String name = Preconditions.checkNotNull(candidateScorer).getIdentifier(); 934 if (name != null) { 935 mCandidateScorers.put(name, candidateScorer); 936 } 937 } 938 939 /** 940 * Unregister a candidate scorer. 941 */ unregisterCandidateScorer(@onNull WifiCandidates.CandidateScorer candidateScorer)942 public void unregisterCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) { 943 String name = Preconditions.checkNotNull(candidateScorer).getIdentifier(); 944 if (name != null) { 945 mCandidateScorers.remove(name); 946 } 947 } 948 949 /** 950 * Derives a numeric experiment identifier from a CandidateScorer's identifier. 951 * 952 * @returns a positive number that starts with the decimal digits ID_PREFIX 953 */ experimentIdFromIdentifier(String id)954 public static int experimentIdFromIdentifier(String id) { 955 final int digits = (int) (((long) id.hashCode()) & Integer.MAX_VALUE) % ID_SUFFIX_MOD; 956 return ID_PREFIX * ID_SUFFIX_MOD + digits; 957 } 958 private static final int ID_SUFFIX_MOD = 1_000_000; 959 private static final int ID_PREFIX = 42; 960 private static final int MIN_SCORER_EXP_ID = ID_PREFIX * ID_SUFFIX_MOD; 961 WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, WifiConfigManager configManager, Clock clock, LocalLog localLog, WifiMetrics wifiMetrics, WifiNative wifiNative)962 WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, 963 WifiConfigManager configManager, Clock clock, LocalLog localLog, 964 WifiMetrics wifiMetrics, WifiNative wifiNative) { 965 mWifiConfigManager = configManager; 966 mClock = clock; 967 mWifiScoreCard = wifiScoreCard; 968 mScoringParams = scoringParams; 969 mLocalLog = localLog; 970 mWifiMetrics = wifiMetrics; 971 mWifiNative = wifiNative; 972 973 mEnableAutoJoinWhenAssociated = context.getResources().getBoolean( 974 R.bool.config_wifi_framework_enable_associated_network_selection); 975 mStayOnNetworkMinimumTxRate = context.getResources().getInteger( 976 R.integer.config_wifi_framework_min_tx_rate_for_staying_on_network); 977 mStayOnNetworkMinimumRxRate = context.getResources().getInteger( 978 R.integer.config_wifi_framework_min_rx_rate_for_staying_on_network); 979 } 980 } 981