content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi;
  
  import static android.net.wifi.WifiManager.WIFI_FEATURE_OWE;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.net.NetworkKey;
  import android.net.wifi.ScanResult;
  import android.net.wifi.SupplicantState;
  import android.net.wifi.WifiConfiguration;
  import android.net.wifi.WifiInfo;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.LocalLog;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.R;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  import com.android.server.wifi.nano.WifiMetricsProto;
  import com.android.server.wifi.util.ScanResultUtil;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  
  /**
   * This class looks at all the connectivity scan results then
   * selects a network for the phone to connect or roam to.
   */
  public class WifiNetworkSelector {
      private static final String TAG = "WifiNetworkSelector";
  
      private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
  
      /**
       * Minimum time gap between last successful network selection and a
       * new selection attempt.
       */
      @VisibleForTesting
      public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
  
      /**
       * For this duration after user selected it, consider the current network as sufficient.
       *
       * This delays network selection during the time that connectivity service may be posting
       * a dialog about a no-internet network.
       */
      @VisibleForTesting
      public static final int LAST_USER_SELECTION_SUFFICIENT_MS = 30_000;
  
      /**
       * Time that it takes for the boost given to the most recently user-selected
       * network to decay to zero.
       *
       * In milliseconds.
       */
      @VisibleForTesting
      public static final int LAST_USER_SELECTION_DECAY_TO_ZERO_MS = 8 * 60 * 60 * 1000;
  
      /**
       * Connected score value used to decide whether a still-connected wifi should be treated
       * as unconnected when filtering scan results.
       */
      @VisibleForTesting
      public static final int WIFI_POOR_SCORE = ConnectedScore.WIFI_TRANSITION_SCORE - 10;
  
      /**
       * The identifier string of the CandidateScorer to use (in the absence of overrides).
       */
      public static final String PRESET_CANDIDATE_SCORER_NAME = "CompatibilityScorer";
  
      /**
       * Experiment ID for the legacy scorer.
       */
      public static final int LEGACY_CANDIDATE_SCORER_EXP_ID = 0;
  
      private final WifiConfigManager mWifiConfigManager;
      private final Clock mClock;
      private final LocalLog mLocalLog;
      private final WifiMetrics mWifiMetrics;
      private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
      // Buffer of filtered scan results (Scan results considered by network selection) & associated
      // WifiConfiguration (if any).
      private final List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
              new ArrayList<>();
      private List<ScanDetail> mFilteredNetworks = new ArrayList<>();
      private final WifiScoreCard mWifiScoreCard;
      private final ScoringParams mScoringParams;
      private final int mStayOnNetworkMinimumTxRate;
      private final int mStayOnNetworkMinimumRxRate;
      private final boolean mEnableAutoJoinWhenAssociated;
      private final WifiNative mWifiNative;
  
      private final Map<String, WifiCandidates.CandidateScorer> mCandidateScorers = new ArrayMap<>();
      private boolean mIsEnhancedOpenSupportedInitialized = false;
      private boolean mIsEnhancedOpenSupported;
  
      /**
       * WiFi Network Selector supports various categories of networks. Each category
       * has an evaluator to choose the best WiFi network to connect to. Evaluators
       * should be registered in order, by decreasing importance.
       * Wifi Network Selector iterates through the registered scorers in registration order
       * before making a final selection from among the candidates.
       */
  
      /**
       * Interface for WiFi Network Evaluator
       *
       * A network evaluator examines the scan results and recommends the
       * best network in its category to connect to; it also reports the
       * connectable candidates in its category for further consideration.
       */
      public interface NetworkEvaluator {
          /** Type of evaluators */
          int EVALUATOR_ID_SAVED = 0;
          int EVALUATOR_ID_SUGGESTION = 1;
          int EVALUATOR_ID_PASSPOINT = 2;
          int EVALUATOR_ID_CARRIER = 3;
          int EVALUATOR_ID_SCORED = 4;
  
          @IntDef(prefix = { "EVALUATOR_ID_" }, value = {
                  EVALUATOR_ID_SAVED,
                  EVALUATOR_ID_SUGGESTION,
                  EVALUATOR_ID_PASSPOINT,
                  EVALUATOR_ID_CARRIER,
                  EVALUATOR_ID_SCORED})
          @Retention(RetentionPolicy.SOURCE)
          public @interface EvaluatorId {}
  
          /**
           * Get the evaluator type.
           */
          @EvaluatorId int getId();
  
          /**
           * Get the evaluator name.
           */
          String getName();
  
          /**
           * Update the evaluator.
           *
           * Certain evaluators have to be updated with the new scan results. For example
           * the ScoredNetworkEvaluator needs to refresh its Score Cache.
           *
           * @param scanDetails    a list of scan details constructed from the scan results
           */
          void update(List<ScanDetail> scanDetails);
  
          /**
           * Evaluate all the networks from the scan results.
           *
           * @param scanDetails    a list of scan details constructed from the scan results
           * @param currentNetwork configuration of the current connected network
           *                       or null if disconnected
           * @param currentBssid   BSSID of the current connected network or null if
           *                       disconnected
           * @param connected      a flag to indicate if ClientModeImpl is in connected
           *                       state
           * @param untrustedNetworkAllowed a flag to indicate if untrusted networks like
           *                                ephemeral networks are allowed
           * @param onConnectableListener callback to record all of the connectable networks
           *
           * @return configuration of the chosen network;
           *         null if no network in this category is available.
           */
          @Nullable
          WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
                          WifiConfiguration currentNetwork, String currentBssid,
                          boolean connected, boolean untrustedNetworkAllowed,
                          OnConnectableListener onConnectableListener);
  
          /**
           * Callback for recording connectable candidates
           */
          public interface OnConnectableListener {
              /**
               * Notes that an access point is an eligible connection candidate
               *
               * @param scanDetail describes the specific access point
               * @param config is the WifiConfiguration for the network
               * @param score is the score assigned by the evaluator
               */
              void onConnectable(ScanDetail scanDetail, WifiConfiguration config, int score);
          }
      }
  
      private final List<NetworkEvaluator> mEvaluators = new ArrayList<>(3);
  
      // A helper to log debugging information in the local log buffer, which can
      // be retrieved in bugreport.
      private void localLog(String log) {
          mLocalLog.log(log);
      }
  
      private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails) {
          // Currently connected?
          if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) {
              localLog("No current connected network.");
              return false;
          } else {
              localLog("Current connected network: " + wifiInfo.getSSID()
                      + " , ID: " + wifiInfo.getNetworkId());
          }
  
          int currentRssi = wifiInfo.getRssi();
          boolean hasQualifiedRssi = currentRssi
                  > mScoringParams.getSufficientRssi(wifiInfo.getFrequency());
          boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate)
                  || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate);
          if (hasQualifiedRssi && hasActiveStream) {
              localLog("Stay on current network because of good RSSI and ongoing traffic");
              return true;
          }
          WifiConfiguration network =
                  mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
  
          if (network == null) {
              localLog("Current network was removed.");
              return false;
          }
  
          if (mWifiConfigManager.getLastSelectedNetwork() == network.networkId
                  && (mClock.getElapsedSinceBootMillis()
                      - mWifiConfigManager.getLastSelectedTimeStamp())
                  <= LAST_USER_SELECTION_SUFFICIENT_MS) {
              localLog("Current network is recently user-selected.");
              return true;
          }
  
          // OSU (Online Sign Up) network for Passpoint Release 2 is sufficient network.
          if (network.osu) {
              return true;
          }
  
          // Ephemeral network is not qualified.
          if (wifiInfo.isEphemeral()) {
              localLog("Current network is an ephemeral one.");
              return false;
          }
  
          if (wifiInfo.is24GHz()) {
              // 2.4GHz networks is not qualified whenever 5GHz is available
              if (is5GHzNetworkAvailable(scanDetails)) {
                  localLog("Current network is 2.4GHz. 5GHz networks available.");
                  return false;
              }
          }
          if (!hasQualifiedRssi) {
              localLog("Current network RSSI[" + currentRssi + "]-acceptable but not qualified.");
              return false;
          }
  
          // Open network is not qualified.
          if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
              localLog("Current network is a open one.");
              return false;
          }
  
          // Network with no internet access reports is not qualified.
          if (network.numNoInternetAccessReports > 0 && !network.noInternetAccessExpected) {
              localLog("Current network has [" + network.numNoInternetAccessReports
                      + "] no-internet access reports.");
              return false;
          }
          return true;
      }
  
      // Determine whether there are any 5GHz networks in the scan result
      private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) {
          for (ScanDetail detail : scanDetails) {
              ScanResult result = detail.getScanResult();
              if (result.is5GHz()) return true;
          }
          return false;
      }
  
      private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
                          boolean connected, boolean disconnected) {
          if (scanDetails.size() == 0) {
              localLog("Empty connectivity scan results. Skip network selection.");
              return false;
          }
  
          if (connected) {
              // Is roaming allowed?
              if (!mEnableAutoJoinWhenAssociated) {
                  localLog("Switching networks in connected state is not allowed."
                          + " Skip network selection.");
                  return false;
              }
  
              // Has it been at least the minimum interval since last network selection?
              if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
                  long gap = mClock.getElapsedSinceBootMillis()
                              - mLastNetworkSelectionTimeStamp;
                  if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) {
                      localLog("Too short since last network selection: " + gap + " ms."
                              + " Skip network selection.");
                      return false;
                  }
              }
  
              if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) {
                  localLog("Current connected network already sufficient. Skip network selection.");
                  return false;
              } else {
                  localLog("Current connected network is not sufficient.");
                  return true;
              }
          } else if (disconnected) {
              return true;
          } else {
              // No network selection if ClientModeImpl is in a state other than
              // CONNECTED or DISCONNECTED.
              localLog("ClientModeImpl is in neither CONNECTED nor DISCONNECTED state."
                      + " Skip network selection.");
              return false;
          }
      }
  
      /**
       * Format the given ScanResult as a scan ID for logging.
       */
      public static String toScanId(@Nullable ScanResult scanResult) {
          return scanResult == null ? "NULL"
                                    : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
      }
  
      /**
       * Format the given WifiConfiguration as a SSID:netId string
       */
      public static String toNetworkString(WifiConfiguration network) {
          if (network == null) {
              return null;
          }
  
          return (network.SSID + ":" + network.networkId);
      }
  
      /**
       * Compares ScanResult level against the minimum threshold for its band, returns true if lower
       */
      public boolean isSignalTooWeak(ScanResult scanResult) {
          return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency));
      }
  
      private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails,
                  HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) {
          ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
          List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
          StringBuffer noValidSsid = new StringBuffer();
          StringBuffer blacklistedBssid = new StringBuffer();
          StringBuffer lowRssi = new StringBuffer();
          boolean scanResultsHaveCurrentBssid = false;
  
          for (ScanDetail scanDetail : scanDetails) {
              ScanResult scanResult = scanDetail.getScanResult();
  
              if (TextUtils.isEmpty(scanResult.SSID)) {
                  noValidSsid.append(scanResult.BSSID).append(" / ");
                  continue;
              }
  
              // Check if the scan results contain the currently connected BSSID
              if (scanResult.BSSID.equals(currentBssid)) {
                  scanResultsHaveCurrentBssid = true;
              }
  
              final String scanId = toScanId(scanResult);
  
              if (bssidBlacklist.contains(scanResult.BSSID)) {
                  blacklistedBssid.append(scanId).append(" / ");
                  continue;
              }
  
              // Skip network with too weak signals.
              if (isSignalTooWeak(scanResult)) {
                  lowRssi.append(scanId).append("(")
                      .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
                      .append(")").append(scanResult.level).append(" / ");
                  continue;
              }
  
              validScanDetails.add(scanDetail);
          }
  
          // WNS listens to all single scan results. Some scan requests may not include
          // the channel of the currently connected network, so the currently connected
          // network won't show up in the scan results. We don't act on these scan results
          // to avoid aggressive network switching which might trigger disconnection.
          if (isConnected && !scanResultsHaveCurrentBssid) {
              localLog("Current connected BSSID " + currentBssid + " is not in the scan results."
                      + " Skip network selection.");
              validScanDetails.clear();
              return validScanDetails;
          }
  
          if (noValidSsid.length() != 0) {
              localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
          }
  
          if (blacklistedBssid.length() != 0) {
              localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
          }
  
          if (lowRssi.length() != 0) {
              localLog("Networks filtered out due to low signal strength: " + lowRssi);
          }
  
          return validScanDetails;
      }
  
      private boolean isEnhancedOpenSupported() {
          if (mIsEnhancedOpenSupportedInitialized) {
              return mIsEnhancedOpenSupported;
          }
  
          mIsEnhancedOpenSupportedInitialized = true;
          mIsEnhancedOpenSupported = (mWifiNative.getSupportedFeatureSet(
                  mWifiNative.getClientInterfaceName()) & WIFI_FEATURE_OWE) != 0;
          return mIsEnhancedOpenSupported;
      }
  
      /**
       * This returns a list of ScanDetails that were filtered in the process of network selection.
       * The list is further filtered for only open unsaved networks.
       *
       * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS,
       * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are
       * no open unsaved networks, or when network selection has not been run.
       */
      public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() {
          List<ScanDetail> openUnsavedNetworks = new ArrayList<>();
          boolean enhancedOpenSupported = isEnhancedOpenSupported();
          for (ScanDetail scanDetail : mFilteredNetworks) {
              ScanResult scanResult = scanDetail.getScanResult();
  
              if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
                  continue;
              }
  
              // Filter out Enhanced Open networks on devices that do not support it
              if (ScanResultUtil.isScanResultForOweNetwork(scanResult)
                      && !enhancedOpenSupported) {
                  continue;
              }
  
              // Skip saved networks
              if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
                  continue;
              }
  
              openUnsavedNetworks.add(scanDetail);
          }
          return openUnsavedNetworks;
      }
  
      /**
       * This returns a list of ScanDetails that were filtered in the process of network selection.
       * The list is further filtered for only carrier unsaved networks with EAP encryption.
       *
       * @param carrierConfig CarrierNetworkConfig used to filter carrier networks
       * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS,
       * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an
       * empty list when there are no such networks, or when network selection has not been run.
       */
      public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks(
              CarrierNetworkConfig carrierConfig) {
          List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>();
          for (ScanDetail scanDetail : mFilteredNetworks) {
              ScanResult scanResult = scanDetail.getScanResult();
  
              if (!ScanResultUtil.isScanResultForEapNetwork(scanResult)
                      || !carrierConfig.isCarrierNetwork(scanResult.SSID)) {
                  continue;
              }
  
              // Skip saved networks
              if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
                  continue;
              }
  
              carrierUnsavedNetworks.add(scanDetail);
          }
          return carrierUnsavedNetworks;
      }
  
      /**
       * @return the list of ScanDetails scored as potential candidates by the last run of
       * selectNetwork, this will be empty if Network selector determined no selection was
       * needed on last run. This includes scan details of sufficient signal strength, and
       * had an associated WifiConfiguration.
       */
      public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() {
          return mConnectableNetworks;
      }
  
      /**
       * This API is called when user explicitly selects a network. Currently, it is used in following
       * cases:
       * (1) User explicitly chooses to connect to a saved network.
       * (2) User saves a network after adding a new network.
       * (3) User saves a network after modifying a saved network.
       * Following actions will be triggered:
       * 1. If this network is disabled, we need re-enable it again.
       * 2. This network is favored over all the other networks visible in latest network
       *    selection procedure.
       *
       * @param netId  ID for the network chosen by the user
       * @return true -- There is change made to connection choice of any saved network.
       *         false -- There is no change made to connection choice of any saved network.
       */
      public boolean setUserConnectChoice(int netId) {
          localLog("userSelectNetwork: network ID=" + netId);
          WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
  
          if (selected == null || selected.SSID == null) {
              localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
              return false;
          }
  
          // Enable the network if it is disabled.
          if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
              mWifiConfigManager.updateNetworkSelectionStatus(netId,
                      WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
          }
          return setLegacyUserConnectChoice(selected);
      }
  
      /**
       * This maintains the legacy user connect choice state in the config store
       */
      private boolean setLegacyUserConnectChoice(@NonNull final WifiConfiguration selected) {
          boolean change = false;
          String key = selected.configKey();
          // This is only used for setting the connect choice timestamp for debugging purposes.
          long currentTime = mClock.getWallClockMillis();
          List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks();
  
          for (WifiConfiguration network : configuredNetworks) {
              WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
              if (network.networkId == selected.networkId) {
                  if (status.getConnectChoice() != null) {
                      localLog("Remove user selection preference of " + status.getConnectChoice()
                              + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
                              + network.SSID + " : " + network.networkId);
                      mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
                      change = true;
                  }
                  continue;
              }
  
              if (status.getSeenInLastQualifiedNetworkSelection()
                          && !key.equals(status.getConnectChoice())) {
                  localLog("Add key: " + key + " Set Time: " + currentTime + " to "
                          + toNetworkString(network));
                  mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
                  change = true;
              }
          }
  
          return change;
      }
  
  
      /**
       * Iterate thru the list of configured networks (includes all saved network configurations +
       * any ephemeral network configurations created for passpoint networks, suggestions, carrier
       * networks, etc) and do the following:
       * a) Try to re-enable any temporarily enabled networks (if the blacklist duration has expired).
       * b) Clear the {@link WifiConfiguration.NetworkSelectionStatus#getCandidate()} field for all
       * of them to identify networks that are present in the current scan result.
       * c) Log any disabled networks.
       */
      private void updateConfiguredNetworks() {
          List<WifiConfiguration> configuredNetworks = mWifiConfigManager.getConfiguredNetworks();
          if (configuredNetworks.size() == 0) {
              localLog("No configured networks.");
              return;
          }
  
          StringBuffer sbuf = new StringBuffer();
          for (WifiConfiguration network : configuredNetworks) {
              // If a configuration is temporarily disabled, re-enable it before trying
              // to connect to it.
              mWifiConfigManager.tryEnableNetwork(network.networkId);
              // Clear the cached candidate, score and seen.
              mWifiConfigManager.clearNetworkCandidateScanResult(network.networkId);
  
              // Log disabled network.
              WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
              if (!status.isNetworkEnabled()) {
                  sbuf.append("  ").append(toNetworkString(network)).append(" ");
                  for (int index = WifiConfiguration.NetworkSelectionStatus
                          .NETWORK_SELECTION_DISABLED_STARTING_INDEX;
                          index < WifiConfiguration.NetworkSelectionStatus
                                  .NETWORK_SELECTION_DISABLED_MAX;
                          index++) {
                      int count = status.getDisableReasonCounter(index);
                      // Here we log the reason as long as its count is greater than zero. The
                      // network may not be disabled because of this particular reason. Logging
                      // this information anyway to help understand what happened to the network.
                      if (count > 0) {
                          sbuf.append("reason=")
                                  .append(WifiConfiguration.NetworkSelectionStatus
                                          .getNetworkDisableReasonString(index))
                                  .append(", count=").append(count).append("; ");
                      }
                  }
                  sbuf.append("\n");
              }
          }
  
          if (sbuf.length() > 0) {
              localLog("Disabled configured networks:");
              localLog(sbuf.toString());
          }
      }
  
      /**
       * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
       * {@link WifiConfiguration} if one exists.
       *
       * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
       */
      private WifiConfiguration overrideCandidateWithUserConnectChoice(
              @NonNull WifiConfiguration candidate) {
          WifiConfiguration tempConfig = Preconditions.checkNotNull(candidate);
          WifiConfiguration originalCandidate = candidate;
          ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
  
          while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
              String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
              tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
  
              if (tempConfig != null) {
                  WifiConfiguration.NetworkSelectionStatus tempStatus =
                          tempConfig.getNetworkSelectionStatus();
                  if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
                      scanResultCandidate = tempStatus.getCandidate();
                      candidate = tempConfig;
                  }
              } else {
                  localLog("Connect choice: " + key + " has no corresponding saved config.");
                  break;
              }
          }
  
          if (candidate != originalCandidate) {
              localLog("After user selection adjustment, the final candidate is:"
                      + WifiNetworkSelector.toNetworkString(candidate) + " : "
                      + scanResultCandidate.BSSID);
              mWifiMetrics.setNominatorForNetwork(candidate.networkId,
                      WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED_USER_CONNECT_CHOICE);
          }
          return candidate;
      }
  
      /**
       * Select the best network from the ones in range.
       *
       * @param scanDetails    List of ScanDetail for all the APs in range
       * @param bssidBlacklist Blacklisted BSSIDs
       * @param wifiInfo       Currently connected network
       * @param connected      True if the device is connected
       * @param disconnected   True if the device is disconnected
       * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection
       * @return Configuration of the selected network, or Null if nothing
       */
      @Nullable
      public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
              HashSet<String> bssidBlacklist, WifiInfo wifiInfo,
              boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
          mFilteredNetworks.clear();
          mConnectableNetworks.clear();
          if (scanDetails.size() == 0) {
              localLog("Empty connectivity scan result");
              return null;
          }
  
          WifiConfiguration currentNetwork =
                  mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
  
          // Always get the current BSSID from WifiInfo in case that firmware initiated
          // roaming happened.
          String currentBssid = wifiInfo.getBSSID();
  
          // Shall we start network selection at all?
          if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
              return null;
          }
  
          // Update all configured networks before initiating network selection.
          updateConfiguredNetworks();
  
          // Update the registered network evaluators.
          for (NetworkEvaluator registeredEvaluator : mEvaluators) {
              registeredEvaluator.update(scanDetails);
          }
  
          // Filter out unwanted networks.
          mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist,
                  connected && wifiInfo.score >= WIFI_POOR_SCORE, currentBssid);
          if (mFilteredNetworks.size() == 0) {
              return null;
          }
  
          // Determine the weight for the last user selection
          final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork();
          final double lastSelectionWeight = calculateLastSelectionWeight();
          final ArraySet<Integer> mNetworkIds = new ArraySet<>();
  
          // Go through the registered network evaluators in order
          WifiConfiguration selectedNetwork = null;
          WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard);
          if (currentNetwork != null) {
              wifiCandidates.setCurrent(currentNetwork.networkId, currentBssid);
          }
          for (NetworkEvaluator registeredEvaluator : mEvaluators) {
              localLog("About to run " + registeredEvaluator.getName() + " :");
              WifiConfiguration choice = registeredEvaluator.evaluateNetworks(
                      new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected,
                      untrustedNetworkAllowed,
                      (scanDetail, config, score) -> {
                          if (config != null) {
                              mConnectableNetworks.add(Pair.create(scanDetail, config));
                              mNetworkIds.add(config.networkId);
                              if (config.networkId == lastUserSelectedNetworkId) {
                                  wifiCandidates.add(scanDetail, config,
                                          registeredEvaluator.getId(), score, lastSelectionWeight);
                              } else {
                                  wifiCandidates.add(scanDetail, config,
                                          registeredEvaluator.getId(), score);
                              }
                              mWifiMetrics.setNominatorForNetwork(config.networkId,
                                      evaluatorIdToNominatorId(registeredEvaluator.getId()));
                          }
                      });
              if (choice != null && !mNetworkIds.contains(choice.networkId)) {
                  Log.wtf(TAG, registeredEvaluator.getName()
                          + " failed to report choice with noConnectibleListener");
              }
              if (selectedNetwork == null && choice != null) {
                  selectedNetwork = choice; // First one wins
                  localLog(registeredEvaluator.getName() + " selects "
                          + WifiNetworkSelector.toNetworkString(selectedNetwork));
              }
          }
  
          if (mConnectableNetworks.size() != wifiCandidates.size()) {
              localLog("Connectable: " + mConnectableNetworks.size()
                      + " Candidates: " + wifiCandidates.size());
          }
  
          // Update the NetworkSelectionStatus in the configs for the current candidates
          // This is needed for the legacy user connect choice, at least
          Collection<Collection<WifiCandidates.Candidate>> groupedCandidates =
                  wifiCandidates.getGroupedCandidates();
          for (Collection<WifiCandidates.Candidate> group: groupedCandidates) {
              WifiCandidates.Candidate best = null;
              for (WifiCandidates.Candidate candidate: group) {
                  // Of all the candidates with the same networkId, choose the
                  // one with the smallest evaluatorId, and break ties by
                  // picking the one with the highest score.
                  if (best == null
                          || candidate.getEvaluatorId() < best.getEvaluatorId()
                          || (candidate.getEvaluatorId() == best.getEvaluatorId()
                              && candidate.getEvaluatorScore() > best.getEvaluatorScore())) {
                      best = candidate;
                  }
              }
              if (best != null) {
                  ScanDetail scanDetail = best.getScanDetail();
                  if (scanDetail != null) {
                      mWifiConfigManager.setNetworkCandidateScanResult(best.getNetworkConfigId(),
                              scanDetail.getScanResult(), best.getEvaluatorScore());
                  }
              }
          }
  
          ArrayMap<Integer, Integer> experimentNetworkSelections = new ArrayMap<>(); // for metrics
  
          final int legacySelectedNetworkId = selectedNetwork == null
                  ? WifiConfiguration.INVALID_NETWORK_ID
                  : selectedNetwork.networkId;
  
          int selectedNetworkId = legacySelectedNetworkId;
  
          // Run all the CandidateScorers
          boolean legacyOverrideWanted = true;
          final WifiCandidates.CandidateScorer activeScorer = getActiveCandidateScorer();
          for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) {
              WifiCandidates.ScoredCandidate choice;
              try {
                  choice = wifiCandidates.choose(candidateScorer);
              } catch (RuntimeException e) {
                  Log.wtf(TAG, "Exception running a CandidateScorer", e);
                  continue;
              }
              int networkId = choice.candidateKey == null
                      ? WifiConfiguration.INVALID_NETWORK_ID
                      : choice.candidateKey.networkId;
              String chooses = " would choose ";
              if (candidateScorer == activeScorer) {
                  chooses = " chooses ";
                  legacyOverrideWanted = candidateScorer.userConnectChoiceOverrideWanted();
                  selectedNetworkId = networkId;
              }
              String id = candidateScorer.getIdentifier();
              int expid = experimentIdFromIdentifier(id);
              localLog(id + chooses + networkId
                      + " score " + choice.value + "+/-" + choice.err
                      + " expid " + expid);
              experimentNetworkSelections.put(expid, networkId);
          }
  
          // Update metrics about differences in the selections made by various methods
          final int activeExperimentId = activeScorer == null ? LEGACY_CANDIDATE_SCORER_EXP_ID
                  : experimentIdFromIdentifier(activeScorer.getIdentifier());
          experimentNetworkSelections.put(LEGACY_CANDIDATE_SCORER_EXP_ID, legacySelectedNetworkId);
          for (Map.Entry<Integer, Integer> entry :
                  experimentNetworkSelections.entrySet()) {
              int experimentId = entry.getKey();
              if (experimentId == activeExperimentId) continue;
              int thisSelectedNetworkId = entry.getValue();
              mWifiMetrics.logNetworkSelectionDecision(experimentId, activeExperimentId,
                      selectedNetworkId == thisSelectedNetworkId,
                      groupedCandidates.size());
          }
  
          // Get a fresh copy of WifiConfiguration reflecting any scan result updates
          selectedNetwork = mWifiConfigManager.getConfiguredNetwork(selectedNetworkId);
          if (selectedNetwork != null && legacyOverrideWanted) {
              selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
              mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
          }
          return selectedNetwork;
      }
  
      private static int evaluatorIdToNominatorId(@NetworkEvaluator.EvaluatorId int evaluatorId) {
          switch (evaluatorId) {
              case NetworkEvaluator.EVALUATOR_ID_SAVED:
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_SAVED;
              case NetworkEvaluator.EVALUATOR_ID_SUGGESTION:
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_SUGGESTION;
              case NetworkEvaluator.EVALUATOR_ID_PASSPOINT:
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_PASSPOINT;
              case NetworkEvaluator.EVALUATOR_ID_CARRIER:
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_CARRIER;
              case NetworkEvaluator.EVALUATOR_ID_SCORED:
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_EXTERNAL_SCORED;
              default:
                  Log.e(TAG, "UnrecognizedEvaluatorId" + evaluatorId);
                  return WifiMetricsProto.ConnectionEvent.NOMINATOR_UNKNOWN;
          }
      }
  
      private double calculateLastSelectionWeight() {
          final int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork();
          double lastSelectionWeight = 0.0;
          if (lastUserSelectedNetworkId != WifiConfiguration.INVALID_NETWORK_ID) {
              double timeDifference = mClock.getElapsedSinceBootMillis()
                      - mWifiConfigManager.getLastSelectedTimeStamp();
              double unclipped = 1.0 - (timeDifference / LAST_USER_SELECTION_DECAY_TO_ZERO_MS);
              lastSelectionWeight = Math.min(Math.max(unclipped, 0.0), 1.0);
          }
          return lastSelectionWeight;
      }
  
      private WifiCandidates.CandidateScorer getActiveCandidateScorer() {
          WifiCandidates.CandidateScorer ans = mCandidateScorers.get(PRESET_CANDIDATE_SCORER_NAME);
          int overrideExperimentId = mScoringParams.getExperimentIdentifier();
          if (overrideExperimentId >= MIN_SCORER_EXP_ID) {
              for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) {
                  int expId = experimentIdFromIdentifier(candidateScorer.getIdentifier());
                  if (expId == overrideExperimentId) {
                      ans = candidateScorer;
                      break;
                  }
              }
          }
          if (ans == null && PRESET_CANDIDATE_SCORER_NAME != null) {
              Log.wtf(TAG, PRESET_CANDIDATE_SCORER_NAME + " is not registered!");
          }
          mWifiMetrics.setNetworkSelectorExperimentId(ans == null
                  ? LEGACY_CANDIDATE_SCORER_EXP_ID
                  : experimentIdFromIdentifier(ans.getIdentifier()));
          return ans;
      }
  
      /**
       * Register a network evaluator
       *
       * @param evaluator the network evaluator to be registered
       *
       */
      public void registerNetworkEvaluator(@NonNull NetworkEvaluator evaluator) {
          mEvaluators.add(Preconditions.checkNotNull(evaluator));
      }
  
      /**
       * Register a candidate scorer.
       *
       * Replaces any existing scorer having the same identifier.
       */
      public void registerCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) {
          String name = Preconditions.checkNotNull(candidateScorer).getIdentifier();
          if (name != null) {
              mCandidateScorers.put(name, candidateScorer);
          }
      }
  
      /**
       * Unregister a candidate scorer.
       */
      public void unregisterCandidateScorer(@NonNull WifiCandidates.CandidateScorer candidateScorer) {
          String name = Preconditions.checkNotNull(candidateScorer).getIdentifier();
          if (name != null) {
              mCandidateScorers.remove(name);
          }
      }
  
      /**
       * Derives a numeric experiment identifier from a CandidateScorer's identifier.
       *
       * @returns a positive number that starts with the decimal digits ID_PREFIX
       */
      public static int experimentIdFromIdentifier(String id) {
          final int digits = (int) (((long) id.hashCode()) & Integer.MAX_VALUE) % ID_SUFFIX_MOD;
          return ID_PREFIX * ID_SUFFIX_MOD + digits;
      }
      private static final int ID_SUFFIX_MOD = 1_000_000;
      private static final int ID_PREFIX = 42;
      private static final int MIN_SCORER_EXP_ID = ID_PREFIX * ID_SUFFIX_MOD;
  
      WifiNetworkSelector(Context context, WifiScoreCard wifiScoreCard, ScoringParams scoringParams,
              WifiConfigManager configManager, Clock clock, LocalLog localLog,
              WifiMetrics wifiMetrics, WifiNative wifiNative) {
          mWifiConfigManager = configManager;
          mClock = clock;
          mWifiScoreCard = wifiScoreCard;
          mScoringParams = scoringParams;
          mLocalLog = localLog;
          mWifiMetrics = wifiMetrics;
          mWifiNative = wifiNative;
  
          mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
                  R.bool.config_wifi_framework_enable_associated_network_selection);
          mStayOnNetworkMinimumTxRate = context.getResources().getInteger(
                  R.integer.config_wifi_framework_min_tx_rate_for_staying_on_network);
          mStayOnNetworkMinimumRxRate = context.getResources().getInteger(
                  R.integer.config_wifi_framework_min_rx_rate_for_staying_on_network);
      }
  }
  