content: /*
   * Copyright 2019 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 com.android.server.wifi.WifiCandidates.Candidate;
  import com.android.server.wifi.WifiCandidates.ScoredCandidate;
  
  import java.util.Collection;
  
  /**
   * A CandidateScorer that weights the RSSIs for more compactly-shaped
   * regions of selection around access points.
   */
  final class BubbleFunScorer implements WifiCandidates.CandidateScorer {
  
      /**
       * This should match WifiNetworkSelector.experimentIdFromIdentifier(getIdentifier())
       * when using the default ScoringParams.
       */
      public static final int BUBBLE_FUN_SCORER_DEFAULT_EXPID = 42598151;
  
      private static final double SECURITY_AWARD = 80.0;
      private static final double CURRENT_NETWORK_BOOST = 80.0;
      private static final double LOW_BAND_FACTOR = 0.3;
      private static final double TYPICAL_SCAN_RSSI_STD = 4.0;
  
      private final ScoringParams mScoringParams;
  
      BubbleFunScorer(ScoringParams scoringParams) {
          mScoringParams = scoringParams;
      }
  
      @Override
      public String getIdentifier() {
          return "BubbleFunScorer_v1";
      }
  
      /**
       * Calculates an individual candidate's score.
       *
       * Ideally, this is a pure function of the candidate, and side-effect free.
       */
      private ScoredCandidate scoreCandidate(Candidate candidate) {
          final int rssi = candidate.getScanRssi();
          final int rssiEntryThreshold = mScoringParams.getEntryRssi(candidate.getFrequency());
  
          double score = shapeFunction(rssi) - shapeFunction(rssiEntryThreshold);
  
          // If we are below the entry threshold, make the score more negative
          if (score < 0.0) score *= 10.0;
  
          // A recently selected network gets a large boost
          score += candidate.getLastSelectionWeight() * CURRENT_NETWORK_BOOST;
  
          // Hysteresis to prefer staying on the current network.
          if (candidate.isCurrentNetwork()) {
              score += CURRENT_NETWORK_BOOST;
          }
  
          if (!candidate.isOpenNetwork()) {
              score += SECURITY_AWARD;
          }
  
          // The gain is approximately the derivative of shapeFunction at the given rssi
          // This is used to estimate the error
          double gain = shapeFunction(rssi + 0.5)
                      - shapeFunction(rssi - 0.5);
  
          // Prefer 5GHz when both are strong, but at the fringes, 2.4 might be better
          // Typically the entry rssi is lower for the 2.4 band, which provides the fringe boost
          if (candidate.getFrequency() < ScoringParams.MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) {
              score *= LOW_BAND_FACTOR;
              gain *= LOW_BAND_FACTOR;
          }
  
          return new ScoredCandidate(score, TYPICAL_SCAN_RSSI_STD * gain, candidate);
      }
  
      /**
       * Reshapes raw RSSI into a value that varies more usefully for scoring purposes.
       *
       * The most important aspect of this function is that it is monotone (has
       * positive slope). The offset and scale are not important, because the
       * calculation above uses differences that cancel out the offset, and
       * a rescaling here effects all the candidates' scores in the same way.
       * However, we choose to scale things for an overall range of about 100 for
       * useful values of RSSI.
       */
      private static double unscaledShapeFunction(double rssi) {
          return -Math.exp(-rssi * BELS_PER_DECIBEL);
      }
      private static final double BELS_PER_DECIBEL = 0.1;
  
      private static final double RESCALE_FACTOR = 100.0 / (
              unscaledShapeFunction(0.0) - unscaledShapeFunction(-85.0));
      private static double shapeFunction(double rssi) {
          return unscaledShapeFunction(rssi) * RESCALE_FACTOR;
      }
  
      @Override
      public ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> group) {
          ScoredCandidate choice = ScoredCandidate.NONE;
          for (Candidate candidate : group) {
              ScoredCandidate scoredCandidate = scoreCandidate(candidate);
              if (scoredCandidate.value > choice.value) {
                  choice = scoredCandidate;
              }
          }
          // Here we just return the highest scored candidate; we could
          // compute a new score, if desired.
          return choice;
      }
  
      @Override
      public boolean userConnectChoiceOverrideWanted() {
          return true;
      }
  
  }
  