content: /*
   * Copyright 2018 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.annotation.Nullable;
  import android.net.MacAddress;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.util.ArrayMap;
  
  import com.android.internal.util.Preconditions;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Map;
  import java.util.Objects;
  import java.util.StringJoiner;
  
  /**
   * Candidates for network selection
   */
  public class WifiCandidates {
      private static final String TAG = "WifiCandidates";
  
      WifiCandidates(@NonNull WifiScoreCard wifiScoreCard) {
          mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard);
      }
      private final WifiScoreCard mWifiScoreCard;
  
      /**
       * Represents a connectable candidate.
       */
      public interface Candidate {
          /**
           * Gets the Key, which contains the SSID, BSSID, security type, and config id.
           *
           * Generally, a CandidateScorer should not need to use this.
           */
          @Nullable Key getKey();
          /**
           * Gets the ScanDetail associate with the candidate.
           */
          @Nullable ScanDetail getScanDetail();
          /**
           * Gets the config id.
           */
          int getNetworkConfigId();
          /**
           * Returns true for an open network.
           */
          boolean isOpenNetwork();
          /**
           * Returns true for a passpoint network.
           */
          boolean isPasspoint();
          /**
           * Returns true for an ephemeral network.
           */
          boolean isEphemeral();
          /**
           * Returns true for a trusted network.
           */
          boolean isTrusted();
          /**
           * Returns the ID of the evaluator that provided the candidate.
           */
          @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId();
          /**
           * Gets the score that was provided by the evaluator.
           *
           * Not all evaluators provide a useful score. Scores from different evaluators
           * are not directly comparable.
           */
          int getEvaluatorScore();
          /**
           * Returns true if the candidate is in the same network as the
           * current connection.
           */
          boolean isCurrentNetwork();
          /**
           * Return true if the candidate is currently connected.
           */
          boolean isCurrentBssid();
          /**
           * Returns a value between 0 and 1.
           *
           * 1.0 means the network was recently selected by the user or an app.
           * 0.0 means not recently selected by user or app.
           */
          double getLastSelectionWeight();
          /**
           * Gets the scan RSSI.
           */
          int getScanRssi();
          /**
           * Gets the scan frequency.
           */
          int getFrequency();
          /**
           * Gets statistics from the scorecard.
           */
          @Nullable WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event);
      }
  
      /**
       * Represents a connectable candidate
       */
      static class CandidateImpl implements Candidate {
          public final Key key;                   // SSID/sectype/BSSID/configId
          public final ScanDetail scanDetail;
          public final WifiConfiguration config;
          // First evaluator to nominate this config
          public final @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId;
          public final int evaluatorScore;        // Score provided by first nominating evaluator
          public final double lastSelectionWeight; // Value between 0 and 1
  
          private WifiScoreCard.PerBssid mPerBssid; // For accessing the scorecard entry
          private final boolean mIsCurrentNetwork;
          private final boolean mIsCurrentBssid;
  
          CandidateImpl(Key key,
                  ScanDetail scanDetail,
                  WifiConfiguration config,
                  @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
                  int evaluatorScore,
                  WifiScoreCard.PerBssid perBssid,
                  double lastSelectionWeight,
                  boolean isCurrentNetwork,
                  boolean isCurrentBssid) {
              this.key = key;
              this.scanDetail = scanDetail;
              this.config = config;
              this.evaluatorId = evaluatorId;
              this.evaluatorScore = evaluatorScore;
              this.mPerBssid = perBssid;
              this.lastSelectionWeight = lastSelectionWeight;
              this.mIsCurrentNetwork = isCurrentNetwork;
              this.mIsCurrentBssid = isCurrentBssid;
          }
  
          @Override
          public Key getKey() {
              return key;
          }
  
          @Override
          public int getNetworkConfigId() {
              return key.networkId;
          }
  
          @Override
          public ScanDetail getScanDetail() {
              return scanDetail;
          }
  
          @Override
          public boolean isOpenNetwork() {
              // TODO - should be able to base this on key.matchInfo.securityType
              return WifiConfigurationUtil.isConfigForOpenNetwork(config);
          }
  
          @Override
          public boolean isPasspoint() {
              return config.isPasspoint();
          }
  
          @Override
          public boolean isEphemeral() {
              return config.ephemeral;
          }
  
          @Override
          public boolean isTrusted() {
              return config.trusted;
          }
  
          @Override
          public @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId() {
              return evaluatorId;
          }
  
          @Override
          public int getEvaluatorScore() {
              return evaluatorScore;
          }
  
          @Override
          public double getLastSelectionWeight() {
              return lastSelectionWeight;
          }
  
          @Override
          public boolean isCurrentNetwork() {
              return mIsCurrentNetwork;
          }
  
          @Override
          public boolean isCurrentBssid() {
              return mIsCurrentBssid;
          }
  
          @Override
          public int getScanRssi() {
              return scanDetail.getScanResult().level;
          }
  
          @Override
          public int getFrequency() {
              return scanDetail.getScanResult().frequency;
          }
  
          /**
           * Accesses statistical information from the score card
           */
          @Override
          public WifiScoreCardProto.Signal
                  getEventStatistics(WifiScoreCardProto.Event event) {
              if (mPerBssid == null) return null;
              WifiScoreCard.PerSignal perSignal = mPerBssid.lookupSignal(event, getFrequency());
              if (perSignal == null) return null;
              return perSignal.toSignal();
          }
  
      }
  
      /**
       * Represents a scoring function
       */
      public interface CandidateScorer {
          /**
           * The scorer's name, and perhaps important parameterization/version.
           */
          String getIdentifier();
  
          /**
           * Calculates the score for a group of candidates that belong
           * to the same network.
           */
          @Nullable ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> group);
  
          /**
           * Returns true if the legacy user connect choice logic should be used.
           *
           * @returns false to disable the legacy logic
           */
          boolean userConnectChoiceOverrideWanted();
      }
  
      /**
       * Represents a candidate with a real-valued score, along with an error estimate.
       *
       * Larger values reflect more desirable candidates. The range is arbitrary,
       * because scores generated by different sources are not compared with each
       * other.
       *
       * The error estimate is on the same scale as the value, and should
       * always be strictly positive. For instance, it might be the standard deviation.
       */
      public static class ScoredCandidate {
          public final double value;
          public final double err;
          public final Key candidateKey;
          public ScoredCandidate(double value, double err, Candidate candidate) {
              this.value = value;
              this.err = err;
              this.candidateKey = (candidate == null) ? null : candidate.getKey();
          }
          /**
           * Represents no score
           */
          public static final ScoredCandidate NONE =
                  new ScoredCandidate(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, null);
      }
  
      /**
       * The key used for tracking candidates, consisting of SSID, security type, BSSID, and network
       * configuration id.
       */
      // TODO (b/123014687) unify with similar classes in the framework
      public static class Key {
          public final ScanResultMatchInfo matchInfo; // Contains the SSID and security type
          public final MacAddress bssid;
          public final int networkId;                 // network configuration id
  
          public Key(ScanResultMatchInfo matchInfo,
                     MacAddress bssid,
                     int networkId) {
              this.matchInfo = matchInfo;
              this.bssid = bssid;
              this.networkId = networkId;
          }
  
          @Override
          public boolean equals(Object other) {
              if (!(other instanceof Key)) return false;
              Key that = (Key) other;
              return (this.matchInfo.equals(that.matchInfo)
                      && this.bssid.equals(that.bssid)
                      && this.networkId == that.networkId);
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(matchInfo, bssid, networkId);
          }
      }
  
      private final Map<Key, CandidateImpl> mCandidates = new ArrayMap<>();
  
      private int mCurrentNetworkId = -1;
      @Nullable private MacAddress mCurrentBssid = null;
  
      /**
       * Sets up information about the currently-connected network.
       */
      public void setCurrent(int currentNetworkId, String currentBssid) {
          mCurrentNetworkId = currentNetworkId;
          mCurrentBssid = null;
          if (currentBssid == null) return;
          try {
              mCurrentBssid = MacAddress.fromString(currentBssid);
          } catch (RuntimeException e) {
              failWithException(e);
          }
      }
  
      /**
       * Adds a new candidate
       *
       * @returns true if added or replaced, false otherwise
       */
      public boolean add(ScanDetail scanDetail,
                      WifiConfiguration config,
                      @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
                      int evaluatorScore,
                      double lastSelectionWeightBetweenZeroAndOne) {
          if (config == null) return failure();
          if (scanDetail == null) return failure();
          ScanResult scanResult = scanDetail.getScanResult();
          if (scanResult == null) return failure();
          MacAddress bssid;
          try {
              bssid = MacAddress.fromString(scanResult.BSSID);
          } catch (RuntimeException e) {
              return failWithException(e);
          }
          ScanResultMatchInfo key1 = ScanResultMatchInfo.fromWifiConfiguration(config);
          ScanResultMatchInfo key2 = ScanResultMatchInfo.fromScanResult(scanResult);
          if (!key1.equals(key2)) return failure(key1, key2);
          Key key = new Key(key1, bssid, config.networkId);
          CandidateImpl old = mCandidates.get(key);
          if (old != null) {
              // check if we want to replace this old candidate
              if (evaluatorId < old.evaluatorId) return failure();
              if (evaluatorId > old.evaluatorId) return false;
              if (evaluatorScore <= old.evaluatorScore) return false;
              remove(old);
          }
          WifiScoreCard.PerBssid perBssid = mWifiScoreCard.lookupBssid(
                  key.matchInfo.networkSsid,
                  key.bssid.toString());
          perBssid.setSecurityType(
                  WifiScoreCardProto.SecurityType.forNumber(key.matchInfo.networkType));
          perBssid.setNetworkConfigId(config.networkId);
          CandidateImpl candidate = new CandidateImpl(key,
                  scanDetail, config, evaluatorId, evaluatorScore, perBssid,
                  Math.min(Math.max(lastSelectionWeightBetweenZeroAndOne, 0.0), 1.0),
                  config.networkId == mCurrentNetworkId,
                  bssid.equals(mCurrentBssid));
          mCandidates.put(key, candidate);
          return true;
      }
      /** Adds a new candidate with no user selection weight. */
      public boolean add(ScanDetail scanDetail,
                      WifiConfiguration config,
                      @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId,
                      int evaluatorScore) {
          return add(scanDetail, config, evaluatorId, evaluatorScore, 0.0);
      }
  
      /**
       * Removes a candidate
       * @returns true if the candidate was successfully removed
       */
      public boolean remove(Candidate candidate) {
          if (!(candidate instanceof CandidateImpl)) return failure();
          return mCandidates.remove(((CandidateImpl) candidate).key, (CandidateImpl) candidate);
      }
  
      /**
       * Returns the number of candidates (at the BSSID level)
       */
      public int size() {
          return mCandidates.size();
      }
  
      /**
       * Returns the candidates, grouped by network.
       */
      public Collection<Collection<Candidate>> getGroupedCandidates() {
          Map<Integer, Collection<Candidate>> candidatesForNetworkId = new ArrayMap<>();
          for (CandidateImpl candidate : mCandidates.values()) {
              Collection<Candidate> cc = candidatesForNetworkId.get(candidate.key.networkId);
              if (cc == null) {
                  cc = new ArrayList<>(2); // Guess 2 bssids per network
                  candidatesForNetworkId.put(candidate.key.networkId, cc);
              }
              cc.add(candidate);
          }
          return candidatesForNetworkId.values();
      }
  
      /**
       * Make a choice from among the candidates, using the provided scorer.
       *
       * @returns the chosen scored candidate, or ScoredCandidate.NONE.
       */
      public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) {
          Preconditions.checkNotNull(candidateScorer);
          ScoredCandidate choice = ScoredCandidate.NONE;
          for (Collection<Candidate> group : getGroupedCandidates()) {
              ScoredCandidate scoredCandidate = candidateScorer.scoreCandidates(group);
              if (scoredCandidate != null && scoredCandidate.value > choice.value) {
                  choice = scoredCandidate;
              }
          }
          return choice;
      }
  
      /**
       * After a failure indication is returned, this may be used to get details.
       */
      public RuntimeException getLastFault() {
          return mLastFault;
      }
  
      /**
       * Returns the number of faults we have seen
       */
      public int getFaultCount() {
          return mFaultCount;
      }
  
      /**
       * Clears any recorded faults
       */
      public void clearFaults() {
          mLastFault = null;
          mFaultCount = 0;
      }
  
      /**
       * Controls whether to immediately raise an exception on a failure
       */
      public WifiCandidates setPicky(boolean picky) {
          mPicky = picky;
          return this;
      }
  
      /**
       * Records details about a failure
       *
       * This captures a stack trace, so don't bother to construct a string message, just
       * supply any culprits (convertible to strings) that might aid diagnosis.
       *
       * @returns false
       * @throws RuntimeException (if in picky mode)
       */
      private boolean failure(Object... culprits) {
          StringJoiner joiner = new StringJoiner(",");
          for (Object c : culprits) {
              joiner.add("" + c);
          }
          return failWithException(new IllegalArgumentException(joiner.toString()));
      }
  
      /**
       * As above, if we already have an exception.
       */
      private boolean failWithException(RuntimeException e) {
          mLastFault = e;
          mFaultCount++;
          if (mPicky) {
              throw e;
          }
          return false;
      }
  
      private boolean mPicky = false;
      private RuntimeException mLastFault = null;
      private int mFaultCount = 0;
  
  }
  