content: /*
   * Copyright (C) 2017 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.net.wifi.WifiInfo;
  
  import com.android.server.wifi.util.KalmanFilter;
  import com.android.server.wifi.util.Matrix;
  
  /**
   * Class used to calculate scores for connected wifi networks and report it to the associated
   * network agent.
   */
  public class VelocityBasedConnectedScore extends ConnectedScore {
  
      private final ScoringParams mScoringParams;
  
      private int mFrequency = ScoringParams.BAND5;
      private double mThresholdAdjustment;
      private final KalmanFilter mFilter;
      private long mLastMillis;
  
      public VelocityBasedConnectedScore(ScoringParams scoringParams, Clock clock) {
          super(clock);
          mScoringParams = scoringParams;
          mFilter = new KalmanFilter();
          mFilter.mH = new Matrix(2, new double[]{1.0, 0.0});
          mFilter.mR = new Matrix(1, new double[]{1.0});
      }
  
      /**
       * Set the Kalman filter's state transition matrix F and process noise covariance Q given
       * a time step.
       *
       * @param dt delta time, in seconds
       */
      private void setDeltaTimeSeconds(double dt) {
          mFilter.mF = new Matrix(2, new double[]{1.0, dt, 0.0, 1.0});
          Matrix tG = new Matrix(1, new double[]{0.5 * dt * dt, dt});
          double stda = 0.02; // standard deviation of modelled acceleration
          mFilter.mQ = tG.dotTranspose(tG).dot(new Matrix(2, new double[]{
                  stda * stda, 0.0,
                  0.0, stda * stda}));
      }
      /**
       * Reset the filter state.
       */
      @Override
      public void reset() {
          mLastMillis = 0;
          mThresholdAdjustment = 0;
          mFilter.mx = null;
      }
  
      /**
       * Updates scoring state using RSSI and measurement noise estimate
       * <p>
       * This is useful if an RSSI comes from another source (e.g. scan results) and the
       * expected noise varies by source.
       *
       * @param rssi              signal strength (dB).
       * @param millis            millisecond-resolution time.
       * @param standardDeviation of the RSSI.
       */
      @Override
      public void updateUsingRssi(int rssi, long millis, double standardDeviation) {
          if (millis <= 0) return;
          if (mLastMillis <= 0 || millis < mLastMillis || mFilter.mx == null) {
              double initialVariance = 9.0 * standardDeviation * standardDeviation;
              mFilter.mx = new Matrix(1, new double[]{rssi, 0.0});
              mFilter.mP = new Matrix(2, new double[]{initialVariance, 0.0, 0.0, 0.0});
          } else {
              double dt = (millis - mLastMillis) * 0.001;
              mFilter.mR.put(0, 0, standardDeviation * standardDeviation);
              setDeltaTimeSeconds(dt);
              mFilter.predict();
              mFilter.update(new Matrix(1, new double[]{rssi}));
          }
          mLastMillis = millis;
          mFilteredRssi = mFilter.mx.get(0, 0);
          mEstimatedRateOfRssiChange = mFilter.mx.get(1, 0);
      }
  
      /**
       * Updates the state.
       */
      @Override
      public void updateUsingWifiInfo(WifiInfo wifiInfo, long millis) {
          int frequency = wifiInfo.getFrequency();
          if (frequency != mFrequency) {
              mLastMillis = 0; // Probably roamed; reset filter but retain threshold adjustment
              // Consider resetting or partially resetting threshold adjustment
              // Consider checking bssid
              mFrequency = frequency;
          }
          updateUsingRssi(wifiInfo.getRssi(), millis, mDefaultRssiStandardDeviation);
          adjustThreshold(wifiInfo);
      }
  
      private double mFilteredRssi;
      private double mEstimatedRateOfRssiChange;
  
      /**
       * Returns the most recently computed extimate of the RSSI.
       */
      public double getFilteredRssi() {
          return mFilteredRssi;
      }
  
      /**
       * Returns the estimated rate of change of RSSI, in dB/second
       */
      public double getEstimatedRateOfRssiChange() {
          return mEstimatedRateOfRssiChange;
      }
  
      /**
       * Returns the adjusted RSSI threshold
       */
      public double getAdjustedRssiThreshold() {
          return mScoringParams.getExitRssi(mFrequency) + mThresholdAdjustment;
      }
  
      private double mMinimumPpsForMeasuringSuccess = 2.0;
  
      /**
       * Adjusts the threshold if appropriate
       * <p>
       * If the (filtered) rssi is near or below the current effective threshold, and the
       * rate of rssi change is small, and there is traffic, and the error rate is looking
       * reasonable, then decrease the effective threshold to keep from dropping a perfectly good
       * connection.
       *
       */
      private void adjustThreshold(WifiInfo wifiInfo) {
          if (mThresholdAdjustment < -7) return;
          if (mFilteredRssi >= getAdjustedRssiThreshold() + 2.0) return;
          if (Math.abs(mEstimatedRateOfRssiChange) >= 0.2) return;
          double txSuccessPps = wifiInfo.txSuccessRate;
          double rxSuccessPps = wifiInfo.rxSuccessRate;
          if (txSuccessPps < mMinimumPpsForMeasuringSuccess) return;
          if (rxSuccessPps < mMinimumPpsForMeasuringSuccess) return;
          double txBadPps = wifiInfo.txBadRate;
          double txRetriesPps = wifiInfo.txRetriesRate;
          double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps + txRetriesPps);
          if (probabilityOfSuccessfulTx > 0.2) {
              // May want this amount to vary with how close to threshold we are
              mThresholdAdjustment -= 0.5;
          }
      }
  
      /**
       * Velocity scorer - predict the rssi a few seconds from now
       */
      @Override
      public int generateScore() {
          if (mFilter.mx == null) return WIFI_TRANSITION_SCORE + 1;
          double badRssi = getAdjustedRssiThreshold();
          double horizonSeconds = mScoringParams.getHorizonSeconds();
          Matrix x = new Matrix(mFilter.mx);
          double filteredRssi = x.get(0, 0);
          setDeltaTimeSeconds(horizonSeconds);
          x = mFilter.mF.dot(x);
          double forecastRssi = x.get(0, 0);
          if (forecastRssi > filteredRssi) {
              forecastRssi = filteredRssi; // Be pessimistic about predicting an actual increase
          }
          int score = (int) (Math.round(forecastRssi) - badRssi) + WIFI_TRANSITION_SCORE;
          return score;
      }
  }
  