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 android.annotation.NonNull;
  import android.content.Context;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.telephony.SubscriptionManager;
  import android.util.LocalLog;
  
  import com.android.internal.R;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.wifi.util.TelephonyUtil;
  
  import java.util.List;
  
  /**
   * This class is the WifiNetworkSelector.NetworkEvaluator implementation for
   * saved networks.
   */
  public class SavedNetworkEvaluator implements WifiNetworkSelector.NetworkEvaluator {
      private static final String NAME = "SavedNetworkEvaluator";
      private final WifiConfigManager mWifiConfigManager;
      private final Clock mClock;
      private final LocalLog mLocalLog;
      private final WifiConnectivityHelper mConnectivityHelper;
      private final SubscriptionManager mSubscriptionManager;
      private final int mRssiScoreSlope;
      private final int mRssiScoreOffset;
      private final int mSameBssidAward;
      private final int mSameNetworkAward;
      private final int mBand5GHzAward;
      private final int mLastSelectionAward;
      private final int mSecurityAward;
      private final ScoringParams mScoringParams;
  
      /**
       * Time it takes for the mLastSelectionAward to decay by one point, in milliseconds
       */
      @VisibleForTesting
      public static final int LAST_SELECTION_AWARD_DECAY_MSEC = 60 * 1000;
  
  
      SavedNetworkEvaluator(final Context context, ScoringParams scoringParams,
              WifiConfigManager configManager, Clock clock,
              LocalLog localLog, WifiConnectivityHelper connectivityHelper,
              SubscriptionManager subscriptionManager) {
          mScoringParams = scoringParams;
          mWifiConfigManager = configManager;
          mClock = clock;
          mLocalLog = localLog;
          mConnectivityHelper = connectivityHelper;
          mSubscriptionManager = subscriptionManager;
  
          mRssiScoreSlope = context.getResources().getInteger(
                  R.integer.config_wifi_framework_RSSI_SCORE_SLOPE);
          mRssiScoreOffset = context.getResources().getInteger(
                  R.integer.config_wifi_framework_RSSI_SCORE_OFFSET);
          mSameBssidAward = context.getResources().getInteger(
                  R.integer.config_wifi_framework_SAME_BSSID_AWARD);
          mSameNetworkAward = context.getResources().getInteger(
                  R.integer.config_wifi_framework_current_network_boost);
          mLastSelectionAward = context.getResources().getInteger(
                  R.integer.config_wifi_framework_LAST_SELECTION_AWARD);
          mSecurityAward = context.getResources().getInteger(
                  R.integer.config_wifi_framework_SECURITY_AWARD);
          mBand5GHzAward = context.getResources().getInteger(
                  R.integer.config_wifi_framework_5GHz_preference_boost_factor);
      }
  
      private void localLog(String log) {
          mLocalLog.log(log);
      }
  
      /**
       * Get the evaluator type.
       */
      @Override
      public @EvaluatorId int getId() {
          return EVALUATOR_ID_SAVED;
      }
  
      /**
       * Get the evaluator name.
       */
      @Override
      public String getName() {
          return NAME;
      }
  
      /**
       * Update the evaluator.
       */
      @Override
      public void update(List<ScanDetail> scanDetails) { }
  
      private int calculateBssidScore(ScanResult scanResult, WifiConfiguration network,
                          WifiConfiguration currentNetwork, String currentBssid,
                          StringBuffer sbuf) {
          int score = 0;
          boolean is5GHz = scanResult.is5GHz();
  
          sbuf.append("[ ").append(scanResult.SSID).append(" ").append(scanResult.BSSID)
                  .append(" RSSI:").append(scanResult.level).append(" ] ");
          // Calculate the RSSI score.
          int rssiSaturationThreshold = mScoringParams.getGoodRssi(scanResult.frequency);
          int rssi = Math.min(scanResult.level, rssiSaturationThreshold);
          score += (rssi + mRssiScoreOffset) * mRssiScoreSlope;
          sbuf.append(" RSSI score: ").append(score).append(",");
  
          // 5GHz band bonus.
          if (is5GHz) {
              score += mBand5GHzAward;
              sbuf.append(" 5GHz bonus: ").append(mBand5GHzAward).append(",");
          }
  
          // Last user selection award.
          int lastUserSelectedNetworkId = mWifiConfigManager.getLastSelectedNetwork();
          if (lastUserSelectedNetworkId != WifiConfiguration.INVALID_NETWORK_ID
                  && lastUserSelectedNetworkId == network.networkId) {
              long timeDifference = mClock.getElapsedSinceBootMillis()
                      - mWifiConfigManager.getLastSelectedTimeStamp();
              if (timeDifference > 0) {
                  int decay = (int) (timeDifference / LAST_SELECTION_AWARD_DECAY_MSEC);
                  int bonus = Math.max(mLastSelectionAward - decay, 0);
                  score += bonus;
                  sbuf.append(" User selection ").append(timeDifference)
                          .append(" ms ago, bonus: ").append(bonus).append(",");
              }
          }
  
          // Same network award.
          if (currentNetwork != null && network.networkId == currentNetwork.networkId) {
              score += mSameNetworkAward;
              sbuf.append(" Same network bonus: ").append(mSameNetworkAward).append(",");
  
              // When firmware roaming is supported, equivalent BSSIDs (the ones under the
              // same network as the currently connected one) get the same BSSID award.
              if (mConnectivityHelper.isFirmwareRoamingSupported()
                      && currentBssid != null && !currentBssid.equals(scanResult.BSSID)) {
                  score += mSameBssidAward;
                  sbuf.append(" Equivalent BSSID bonus: ").append(mSameBssidAward).append(",");
              }
          }
  
          // Same BSSID award.
          if (currentBssid != null && currentBssid.equals(scanResult.BSSID)) {
              score += mSameBssidAward;
              sbuf.append(" Same BSSID bonus: ").append(mSameBssidAward).append(",");
          }
  
          // Security award.
          if (!WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
              score += mSecurityAward;
              sbuf.append(" Secure network bonus: ").append(mSecurityAward).append(",");
          }
  
          sbuf.append(" ## Total score: ").append(score).append("\n");
  
          return score;
      }
  
      /**
       * Evaluate all the networks from the scan results and return
       * the WifiConfiguration of the network chosen for connection.
       *
       * @return configuration of the chosen network;
       *         null if no network in this category is available.
       */
      @Override
      public WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
                      WifiConfiguration currentNetwork, String currentBssid, boolean connected,
                      boolean untrustedNetworkAllowed,
                      @NonNull OnConnectableListener onConnectableListener) {
          int highestScore = Integer.MIN_VALUE;
          ScanResult scanResultCandidate = null;
          WifiConfiguration candidate = null;
          StringBuffer scoreHistory = new StringBuffer();
  
          for (ScanDetail scanDetail : scanDetails) {
              ScanResult scanResult = scanDetail.getScanResult();
  
              // One ScanResult can be associated with more than one network, hence we calculate all
              // the scores and use the highest one as the ScanResult's score.
              // TODO(b/112196799): this has side effects, rather not do that in an evaluator
              WifiConfiguration network =
                      mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail);
  
              if (network == null) {
                  continue;
              }
  
              /**
               * Ignore Passpoint and Ephemeral networks. They are configured networks,
               * but without being persisted to the storage. They are evaluated by
               * {@link PasspointNetworkEvaluator} and {@link ScoredNetworkEvaluator}
               * respectively.
               */
              if (network.isPasspoint() || network.isEphemeral()) {
                  continue;
              }
  
              WifiConfiguration.NetworkSelectionStatus status =
                      network.getNetworkSelectionStatus();
              // TODO (b/112196799): another side effect
              status.setSeenInLastQualifiedNetworkSelection(true);
  
              if (!status.isNetworkEnabled()) {
                  continue;
              } else if (network.BSSID != null &&  !network.BSSID.equals("any")
                      && !network.BSSID.equals(scanResult.BSSID)) {
                  // App has specified the only BSSID to connect for this
                  // configuration. So only the matching ScanResult can be a candidate.
                  localLog("Network " + WifiNetworkSelector.toNetworkString(network)
                          + " has specified BSSID " + network.BSSID + ". Skip "
                          + scanResult.BSSID);
                  continue;
              } else if (TelephonyUtil.isSimConfig(network)
                      && !TelephonyUtil.isSimPresent(mSubscriptionManager)) {
                  // Don't select if security type is EAP SIM/AKA/AKA' when SIM is not present.
                  continue;
              }
  
              int score = calculateBssidScore(scanResult, network, currentNetwork, currentBssid,
                      scoreHistory);
  
              // Set candidate ScanResult for all saved networks to ensure that users can
              // override network selection. See WifiNetworkSelector#setUserConnectChoice.
              if (score > status.getCandidateScore()
                      || (score == status.getCandidateScore()
                          && status.getCandidate() != null
                          && scanResult.level > status.getCandidate().level)) {
                  mWifiConfigManager.setNetworkCandidateScanResult(
                          network.networkId, scanResult, score);
              }
  
              // If the network is marked to use external scores, or is an open network with
              // curate saved open networks enabled, do not consider it for network selection.
              if (network.useExternalScores) {
                  localLog("Network " + WifiNetworkSelector.toNetworkString(network)
                          + " has external score.");
                  continue;
              }
  
              onConnectableListener.onConnectable(scanDetail,
                      mWifiConfigManager.getConfiguredNetwork(network.networkId), score);
  
              // TODO(b/112196799) - pull into common code
              if (score > highestScore
                      || (score == highestScore
                      && scanResultCandidate != null
                      && scanResult.level > scanResultCandidate.level)) {
                  highestScore = score;
                  scanResultCandidate = scanResult;
                  mWifiConfigManager.setNetworkCandidateScanResult(
                          network.networkId, scanResultCandidate, highestScore);
                  // Reload the network config with the updated info.
                  candidate = mWifiConfigManager.getConfiguredNetwork(network.networkId);
              }
          }
  
          if (scoreHistory.length() > 0) {
              localLog("\n" + scoreHistory.toString());
          }
  
          if (scanResultCandidate == null) {
              localLog("did not see any good candidates.");
          }
          return candidate;
      }
  }
  