blob: 8a02a82194df5408ab455bdaa602b3fcd7ad2c90 [file] [log] [blame]
/*
* 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.internal.location.gnssmetrics;
import android.os.SystemClock;
import android.os.connectivity.GpsBatteryStats;
import android.os.SystemProperties;
import android.text.format.DateUtils;
import android.util.Base64;
import android.util.Log;
import android.util.TimeUtils;
import java.util.Arrays;
import com.android.internal.app.IBatteryStats;
import com.android.internal.location.nano.GnssLogsProto.GnssLog;
import com.android.internal.location.nano.GnssLogsProto.PowerMetrics;
/**
* GnssMetrics: Is used for logging GNSS metrics
* @hide
*/
public class GnssMetrics {
private static final String TAG = GnssMetrics.class.getSimpleName();
/* Constant which indicates GPS signal quality is poor */
public static final int GPS_SIGNAL_QUALITY_POOR = 0;
/* Constant which indicates GPS signal quality is good */
public static final int GPS_SIGNAL_QUALITY_GOOD = 1;
/* Number of GPS signal quality levels */
public static final int NUM_GPS_SIGNAL_QUALITY_LEVELS = GPS_SIGNAL_QUALITY_GOOD + 1;
/** Default time between location fixes (in millisecs) */
private static final int DEFAULT_TIME_BETWEEN_FIXES_MILLISECS = 1000;
/* The time since boot when logging started */
private String logStartInElapsedRealTime;
/* GNSS power metrics */
private GnssPowerMetrics mGnssPowerMetrics;
/** Constructor */
public GnssMetrics(IBatteryStats stats) {
mGnssPowerMetrics = new GnssPowerMetrics(stats);
locationFailureStatistics = new Statistics();
timeToFirstFixSecStatistics = new Statistics();
positionAccuracyMeterStatistics = new Statistics();
topFourAverageCn0Statistics = new Statistics();
reset();
}
/**
* Logs the status of a location report received from the HAL
*
* @param isSuccessful
*/
public void logReceivedLocationStatus(boolean isSuccessful) {
if (!isSuccessful) {
locationFailureStatistics.addItem(1.0);
return;
}
locationFailureStatistics.addItem(0.0);
return;
}
/**
* Logs missed reports
*
* @param desiredTimeBetweenFixesMilliSeconds
* @param actualTimeBetweenFixesMilliSeconds
*/
public void logMissedReports(int desiredTimeBetweenFixesMilliSeconds,
int actualTimeBetweenFixesMilliSeconds) {
int numReportMissed = (actualTimeBetweenFixesMilliSeconds /
Math.max(DEFAULT_TIME_BETWEEN_FIXES_MILLISECS, desiredTimeBetweenFixesMilliSeconds)) - 1;
if (numReportMissed > 0) {
for (int i = 0; i < numReportMissed; i++) {
locationFailureStatistics.addItem(1.0);
}
}
return;
}
/**
* Logs time to first fix
*
* @param timeToFirstFixMilliSeconds
*/
public void logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds) {
timeToFirstFixSecStatistics.addItem((double) (timeToFirstFixMilliSeconds/1000));
return;
}
/**
* Logs position accuracy
*
* @param positionAccuracyMeters
*/
public void logPositionAccuracyMeters(float positionAccuracyMeters) {
positionAccuracyMeterStatistics.addItem((double) positionAccuracyMeters);
return;
}
/*
* Logs CN0 when at least 4 SVs are available
*
*/
public void logCn0(float[] cn0s, int numSv) {
if (numSv == 0 || cn0s == null || cn0s.length == 0 || cn0s.length < numSv) {
if (numSv == 0) {
mGnssPowerMetrics.reportSignalQuality(null, 0);
}
return;
}
float[] cn0Array = Arrays.copyOf(cn0s, numSv);
Arrays.sort(cn0Array);
mGnssPowerMetrics.reportSignalQuality(cn0Array, numSv);
if (numSv < 4) {
return;
}
if (cn0Array[numSv - 4] > 0.0) {
double top4AvgCn0 = 0.0;
for (int i = numSv - 4; i < numSv; i++) {
top4AvgCn0 += (double) cn0Array[i];
}
top4AvgCn0 /= 4;
topFourAverageCn0Statistics.addItem(top4AvgCn0);
}
return;
}
/**
* Dumps GNSS metrics as a proto string
* @return
*/
public String dumpGnssMetricsAsProtoString() {
GnssLog msg = new GnssLog();
if (locationFailureStatistics.getCount() > 0) {
msg.numLocationReportProcessed = locationFailureStatistics.getCount();
msg.percentageLocationFailure = (int) (100.0 * locationFailureStatistics.getMean());
}
if (timeToFirstFixSecStatistics.getCount() > 0) {
msg.numTimeToFirstFixProcessed = timeToFirstFixSecStatistics.getCount();
msg.meanTimeToFirstFixSecs = (int) timeToFirstFixSecStatistics.getMean();
msg.standardDeviationTimeToFirstFixSecs
= (int) timeToFirstFixSecStatistics.getStandardDeviation();
}
if (positionAccuracyMeterStatistics.getCount() > 0) {
msg.numPositionAccuracyProcessed = positionAccuracyMeterStatistics.getCount();
msg.meanPositionAccuracyMeters = (int) positionAccuracyMeterStatistics.getMean();
msg.standardDeviationPositionAccuracyMeters
= (int) positionAccuracyMeterStatistics.getStandardDeviation();
}
if (topFourAverageCn0Statistics.getCount() > 0) {
msg.numTopFourAverageCn0Processed = topFourAverageCn0Statistics.getCount();
msg.meanTopFourAverageCn0DbHz = topFourAverageCn0Statistics.getMean();
msg.standardDeviationTopFourAverageCn0DbHz
= topFourAverageCn0Statistics.getStandardDeviation();
}
msg.powerMetrics = mGnssPowerMetrics.buildProto();
msg.hardwareRevision = SystemProperties.get("ro.boot.revision", "");
String s = Base64.encodeToString(GnssLog.toByteArray(msg), Base64.DEFAULT);
reset();
return s;
}
/**
* Dumps GNSS Metrics as text
*
* @return GNSS Metrics
*/
public String dumpGnssMetricsAsText() {
StringBuilder s = new StringBuilder();
s.append("GNSS_KPI_START").append('\n');
s.append(" KPI logging start time: ").append(logStartInElapsedRealTime).append("\n");
s.append(" KPI logging end time: ");
TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
s.append("\n");
s.append(" Number of location reports: ").append(
locationFailureStatistics.getCount()).append("\n");
if (locationFailureStatistics.getCount() > 0) {
s.append(" Percentage location failure: ").append(
100.0 * locationFailureStatistics.getMean()).append("\n");
}
s.append(" Number of TTFF reports: ").append(
timeToFirstFixSecStatistics.getCount()).append("\n");
if (timeToFirstFixSecStatistics.getCount() > 0) {
s.append(" TTFF mean (sec): ").append(timeToFirstFixSecStatistics.getMean()).append("\n");
s.append(" TTFF standard deviation (sec): ").append(
timeToFirstFixSecStatistics.getStandardDeviation()).append("\n");
}
s.append(" Number of position accuracy reports: ").append(
positionAccuracyMeterStatistics.getCount()).append("\n");
if (positionAccuracyMeterStatistics.getCount() > 0) {
s.append(" Position accuracy mean (m): ").append(
positionAccuracyMeterStatistics.getMean()).append("\n");
s.append(" Position accuracy standard deviation (m): ").append(
positionAccuracyMeterStatistics.getStandardDeviation()).append("\n");
}
s.append(" Number of CN0 reports: ").append(
topFourAverageCn0Statistics.getCount()).append("\n");
if (topFourAverageCn0Statistics.getCount() > 0) {
s.append(" Top 4 Avg CN0 mean (dB-Hz): ").append(
topFourAverageCn0Statistics.getMean()).append("\n");
s.append(" Top 4 Avg CN0 standard deviation (dB-Hz): ").append(
topFourAverageCn0Statistics.getStandardDeviation()).append("\n");
}
s.append("GNSS_KPI_END").append("\n");
GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
if (stats != null) {
s.append("Power Metrics").append("\n");
s.append(" Time on battery (min): "
+ stats.getLoggingDurationMs() / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
long[] t = stats.getTimeInGpsSignalQualityLevel();
if (t != null && t.length == NUM_GPS_SIGNAL_QUALITY_LEVELS) {
s.append(" Amount of time (while on battery) Top 4 Avg CN0 > " +
Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
" dB-Hz (min): ").append(t[1] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
s.append(" Amount of time (while on battery) Top 4 Avg CN0 <= " +
Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
" dB-Hz (min): ").append(t[0] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
}
s.append(" Energy consumed while on battery (mAh): ").append(
stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS)).append("\n");
}
s.append("Hardware Version: " + SystemProperties.get("ro.boot.revision", "")).append("\n");
return s.toString();
}
/** Class for storing statistics */
private class Statistics {
/** Resets statistics */
public void reset() {
count = 0;
sum = 0.0;
sumSquare = 0.0;
}
/** Adds an item */
public void addItem(double item) {
count++;
sum += item;
sumSquare += item * item;
}
/** Returns number of items added */
public int getCount() {
return count;
}
/** Returns mean */
public double getMean() {
return sum/count;
}
/** Returns standard deviation */
public double getStandardDeviation() {
double m = sum/count;
m = m * m;
double v = sumSquare/count;
if (v > m) {
return Math.sqrt(v - m);
}
return 0;
}
private int count;
private double sum;
private double sumSquare;
}
/** Location failure statistics */
private Statistics locationFailureStatistics;
/** Time to first fix statistics */
private Statistics timeToFirstFixSecStatistics;
/** Position accuracy statistics */
private Statistics positionAccuracyMeterStatistics;
/** Top 4 average CN0 statistics */
private Statistics topFourAverageCn0Statistics;
/**
* Resets GNSS metrics
*/
private void reset() {
StringBuilder s = new StringBuilder();
TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
logStartInElapsedRealTime = s.toString();
locationFailureStatistics.reset();
timeToFirstFixSecStatistics.reset();
positionAccuracyMeterStatistics.reset();
topFourAverageCn0Statistics.reset();
return;
}
/* Class for handling GNSS power related metrics */
private class GnssPowerMetrics {
/* Threshold for Top Four Average CN0 below which GNSS signal quality is declared poor */
public static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0;
/* Minimum change in Top Four Average CN0 needed to trigger a report */
private static final double REPORTING_THRESHOLD_DB_HZ = 1.0;
/* BatteryStats API */
private final IBatteryStats mBatteryStats;
/* Last reported Top Four Average CN0 */
private double mLastAverageCn0;
public GnssPowerMetrics(IBatteryStats stats) {
mBatteryStats = stats;
// Used to initialize the variable to a very small value (unachievable in practice) so that
// the first CNO report will trigger an update to BatteryStats
mLastAverageCn0 = -100.0;
}
/**
* Builds power metrics proto buf. This is included in the gnss proto buf.
* @return PowerMetrics
*/
public PowerMetrics buildProto() {
PowerMetrics p = new PowerMetrics();
GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
if (stats != null) {
p.loggingDurationMs = stats.getLoggingDurationMs();
p.energyConsumedMah = stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS);
long[] t = stats.getTimeInGpsSignalQualityLevel();
p.timeInSignalQualityLevelMs = new long[t.length];
for (int i = 0; i < t.length; i++) {
p.timeInSignalQualityLevelMs[i] = t[i];
}
}
return p;
}
/**
* Returns the GPS power stats
* @return GpsBatteryStats
*/
public GpsBatteryStats getGpsBatteryStats() {
try {
return mBatteryStats.getGpsBatteryStats();
} catch (Exception e) {
Log.w(TAG, "Exception", e);
return null;
}
}
/**
* Reports signal quality to BatteryStats. Signal quality is based on Top four average CN0. If
* the number of SVs seen is less than 4, then signal quality is the average CN0.
* Changes are reported only if the average CN0 changes by more than REPORTING_THRESHOLD_DB_HZ.
*/
public void reportSignalQuality(float[] ascendingCN0Array, int numSv) {
double avgCn0 = 0.0;
if (numSv > 0) {
for (int i = Math.max(0, numSv - 4); i < numSv; i++) {
avgCn0 += (double) ascendingCN0Array[i];
}
avgCn0 /= Math.min(numSv, 4);
}
if (Math.abs(avgCn0 - mLastAverageCn0) < REPORTING_THRESHOLD_DB_HZ) {
return;
}
try {
mBatteryStats.noteGpsSignalQuality(getSignalLevel(avgCn0));
mLastAverageCn0 = avgCn0;
} catch (Exception e) {
Log.w(TAG, "Exception", e);
}
return;
}
/**
* Obtains signal level based on CN0
*/
private int getSignalLevel(double cn0) {
if (cn0 > POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) {
return GnssMetrics.GPS_SIGNAL_QUALITY_GOOD;
}
return GnssMetrics.GPS_SIGNAL_QUALITY_POOR;
}
}
}