content: /*
   * Copyright (C) 2018 Google Inc. All Rights Reserved.
   *
   * 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.example.android.wifirttscan;
  
  import android.Manifest.permission;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.net.wifi.ScanResult;
  import android.net.wifi.rtt.RangingRequest;
  import android.net.wifi.rtt.RangingResult;
  import android.net.wifi.rtt.RangingResultCallback;
  import android.net.wifi.rtt.WifiRttManager;
  import android.os.Bundle;
  import android.os.Handler;
  import androidx.annotation.NonNull;
  import androidx.core.app.ActivityCompat;
  import androidx.appcompat.app.AppCompatActivity;
  import android.util.Log;
  import android.view.View;
  import android.widget.EditText;
  import android.widget.TextView;
  import android.widget.Toast;
  
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Displays ranging information about a particular access point chosen by the user. Uses {@link
   * Handler} to trigger new requests based on
   */
  public class AccessPointRangingResultsActivity extends AppCompatActivity {
      private static final String TAG = "APRRActivity";
  
      public static final String SCAN_RESULT_EXTRA =
              "com.example.android.wifirttscan.extra.SCAN_RESULT";
  
      private static final int SAMPLE_SIZE_DEFAULT = 50;
      private static final int MILLISECONDS_DELAY_BEFORE_NEW_RANGING_REQUEST_DEFAULT = 1000;
  
      // UI Elements.
      private TextView mSsidTextView;
      private TextView mBssidTextView;
  
      private TextView mRangeTextView;
      private TextView mRangeMeanTextView;
      private TextView mRangeSDTextView;
      private TextView mRangeSDMeanTextView;
      private TextView mRssiTextView;
      private TextView mSuccessesInBurstTextView;
      private TextView mSuccessRatioTextView;
      private TextView mNumberOfRequestsTextView;
  
      private EditText mSampleSizeEditText;
      private EditText mMillisecondsDelayBeforeNewRangingRequestEditText;
  
      // Non UI variables.
      private ScanResult mScanResult;
      private String mMAC;
  
      private int mNumberOfRangeRequests;
      private int mNumberOfSuccessfulRangeRequests;
  
      private int mMillisecondsDelayBeforeNewRangingRequest;
  
      // Max sample size to calculate average for
      // 1. Distance to device (getDistanceMm) over time
      // 2. Standard deviation of the measured distance to the device (getDistanceStdDevMm) over time
      // Note: A RangeRequest result already consists of the average of 7 readings from a burst,
      // so the average in (1) is the average of these averages.
      private int mSampleSize;
  
      // Used to loop over a list of distances to calculate averages (ensures data structure never
      // get larger than sample size).
      private int mStatisticRangeHistoryEndIndex;
      private ArrayList<Integer> mStatisticRangeHistory;
  
      // Used to loop over a list of the standard deviation of the measured distance to calculate
      // averages  (ensures data structure never get larger than sample size).
      private int mStatisticRangeSDHistoryEndIndex;
      private ArrayList<Integer> mStatisticRangeSDHistory;
  
      private WifiRttManager mWifiRttManager;
      private RttRangingResultCallback mRttRangingResultCallback;
  
      // Triggers additional RangingRequests with delay (mMillisecondsDelayBeforeNewRangingRequest).
      final Handler mRangeRequestDelayHandler = new Handler();
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_access_point_ranging_results);
  
          // Initializes UI elements.
          mSsidTextView = findViewById(R.id.ssid);
          mBssidTextView = findViewById(R.id.bssid);
  
          mRangeTextView = findViewById(R.id.range_value);
          mRangeMeanTextView = findViewById(R.id.range_mean_value);
          mRangeSDTextView = findViewById(R.id.range_sd_value);
          mRangeSDMeanTextView = findViewById(R.id.range_sd_mean_value);
          mRssiTextView = findViewById(R.id.rssi_value);
          mSuccessesInBurstTextView = findViewById(R.id.successes_in_burst_value);
          mSuccessRatioTextView = findViewById(R.id.success_ratio_value);
          mNumberOfRequestsTextView = findViewById(R.id.number_of_requests_value);
  
          mSampleSizeEditText = findViewById(R.id.stats_window_size_edit_value);
          mSampleSizeEditText.setText(SAMPLE_SIZE_DEFAULT + "");
  
          mMillisecondsDelayBeforeNewRangingRequestEditText =
                  findViewById(R.id.ranging_period_edit_value);
          mMillisecondsDelayBeforeNewRangingRequestEditText.setText(
                  MILLISECONDS_DELAY_BEFORE_NEW_RANGING_REQUEST_DEFAULT + "");
  
          // Retrieve ScanResult from Intent.
          Intent intent = getIntent();
          mScanResult = intent.getParcelableExtra(SCAN_RESULT_EXTRA);
  
          if (mScanResult == null) {
              finish();
          }
  
          mMAC = mScanResult.BSSID;
  
          mSsidTextView.setText(mScanResult.SSID);
          mBssidTextView.setText(mScanResult.BSSID);
  
          mWifiRttManager = (WifiRttManager) getSystemService(Context.WIFI_RTT_RANGING_SERVICE);
          mRttRangingResultCallback = new RttRangingResultCallback();
  
          // Used to store range (distance) and rangeSd (standard deviation of the measured distance)
          // history to calculate averages.
          mStatisticRangeHistory = new ArrayList<>();
          mStatisticRangeSDHistory = new ArrayList<>();
  
          resetData();
  
          startRangingRequest();
      }
  
      private void resetData() {
          mSampleSize = Integer.parseInt(mSampleSizeEditText.getText().toString());
  
          mMillisecondsDelayBeforeNewRangingRequest =
                  Integer.parseInt(
                          mMillisecondsDelayBeforeNewRangingRequestEditText.getText().toString());
  
          mNumberOfSuccessfulRangeRequests = 0;
          mNumberOfRangeRequests = 0;
  
          mStatisticRangeHistoryEndIndex = 0;
          mStatisticRangeHistory.clear();
  
          mStatisticRangeSDHistoryEndIndex = 0;
          mStatisticRangeSDHistory.clear();
      }
  
      private void startRangingRequest() {
          // Permission for fine location should already be granted via MainActivity (you can't get
          // to this class unless you already have permission. If they get to this class, then disable
          // fine location permission, we kick them back to main activity.
          if (ActivityCompat.checkSelfPermission(this, permission.ACCESS_FINE_LOCATION)
                  != PackageManager.PERMISSION_GRANTED) {
              finish();
          }
  
          mNumberOfRangeRequests++;
  
          RangingRequest rangingRequest =
                  new RangingRequest.Builder().addAccessPoint(mScanResult).build();
  
          mWifiRttManager.startRanging(
                  rangingRequest, getApplication().getMainExecutor(), mRttRangingResultCallback);
      }
  
      // Calculates average distance based on stored history.
      private float getDistanceMean() {
          float distanceSum = 0;
  
          for (int distance : mStatisticRangeHistory) {
              distanceSum += distance;
          }
  
          return distanceSum / mStatisticRangeHistory.size();
      }
  
      // Adds distance to history. If larger than sample size value, loops back over and replaces the
      // oldest distance record in the list.
      private void addDistanceToHistory(int distance) {
  
          if (mStatisticRangeHistory.size() >= mSampleSize) {
  
              if (mStatisticRangeHistoryEndIndex >= mSampleSize) {
                  mStatisticRangeHistoryEndIndex = 0;
              }
  
              mStatisticRangeHistory.set(mStatisticRangeHistoryEndIndex, distance);
              mStatisticRangeHistoryEndIndex++;
  
          } else {
              mStatisticRangeHistory.add(distance);
          }
      }
  
      // Calculates standard deviation of the measured distance based on stored history.
      private float getStandardDeviationOfDistanceMean() {
          float distanceSdSum = 0;
  
          for (int distanceSd : mStatisticRangeSDHistory) {
              distanceSdSum += distanceSd;
          }
  
          return distanceSdSum / mStatisticRangeHistory.size();
      }
  
      // Adds standard deviation of the measured distance to history. If larger than sample size
      // value, loops back over and replaces the oldest distance record in the list.
      private void addStandardDeviationOfDistanceToHistory(int distanceSd) {
  
          if (mStatisticRangeSDHistory.size() >= mSampleSize) {
  
              if (mStatisticRangeSDHistoryEndIndex >= mSampleSize) {
                  mStatisticRangeSDHistoryEndIndex = 0;
              }
  
              mStatisticRangeSDHistory.set(mStatisticRangeSDHistoryEndIndex, distanceSd);
              mStatisticRangeSDHistoryEndIndex++;
  
          } else {
              mStatisticRangeSDHistory.add(distanceSd);
          }
      }
  
      public void onResetButtonClick(View view) {
          resetData();
      }
  
      // Class that handles callbacks for all RangingRequests and issues new RangingRequests.
      private class RttRangingResultCallback extends RangingResultCallback {
  
          private void queueNextRangingRequest() {
              mRangeRequestDelayHandler.postDelayed(
                      new Runnable() {
                          @Override
                          public void run() {
                              startRangingRequest();
                          }
                      },
                      mMillisecondsDelayBeforeNewRangingRequest);
          }
  
          @Override
          public void onRangingFailure(int code) {
              Log.d(TAG, "onRangingFailure() code: " + code);
              queueNextRangingRequest();
          }
  
          @Override
          public void onRangingResults(@NonNull List<RangingResult> list) {
              Log.d(TAG, "onRangingResults(): " + list);
  
              // Because we are only requesting RangingResult for one access point (not multiple
              // access points), this will only ever be one. (Use loops when requesting RangingResults
              // for multiple access points.)
              if (list.size() == 1) {
  
                  RangingResult rangingResult = list.get(0);
  
                  if (mMAC.equals(rangingResult.getMacAddress().toString())) {
  
                      if (rangingResult.getStatus() == RangingResult.STATUS_SUCCESS) {
  
                          mNumberOfSuccessfulRangeRequests++;
  
                          mRangeTextView.setText((rangingResult.getDistanceMm() / 1000f) + "");
                          addDistanceToHistory(rangingResult.getDistanceMm());
                          mRangeMeanTextView.setText((getDistanceMean() / 1000f) + "");
  
                          mRangeSDTextView.setText(
                                  (rangingResult.getDistanceStdDevMm() / 1000f) + "");
                          addStandardDeviationOfDistanceToHistory(
                                  rangingResult.getDistanceStdDevMm());
                          mRangeSDMeanTextView.setText(
                                  (getStandardDeviationOfDistanceMean() / 1000f) + "");
  
                          mRssiTextView.setText(rangingResult.getRssi() + "");
                          mSuccessesInBurstTextView.setText(
                                  rangingResult.getNumSuccessfulMeasurements()
                                          + "/"
                                          + rangingResult.getNumAttemptedMeasurements());
  
                          float successRatio =
                                  ((float) mNumberOfSuccessfulRangeRequests
                                                  / (float) mNumberOfRangeRequests)
                                          * 100;
                          mSuccessRatioTextView.setText(successRatio + "%");
  
                          mNumberOfRequestsTextView.setText(mNumberOfRangeRequests + "");
  
                      } else if (rangingResult.getStatus()
                              == RangingResult.STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC) {
                          Log.d(TAG, "RangingResult failed (AP doesn't support IEEE80211 MC.");
  
                      } else {
                          Log.d(TAG, "RangingResult failed.");
                      }
  
                  } else {
                      Toast.makeText(
                                      getApplicationContext(),
                                      R.string
                                              .mac_mismatch_message_activity_access_point_ranging_results,
                                      Toast.LENGTH_LONG)
                              .show();
                  }
              }
  
              queueNextRangingRequest();
          }
      }
  }
  