/*
 * 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
/** * 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)
/** 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
/** 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
Params:
  • isSuccessful –
/** * 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
Params:
  • desiredTimeBetweenFixesMilliSeconds –
  • actualTimeBetweenFixesMilliSeconds –
/** * 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
Params:
  • timeToFirstFixMilliSeconds –
/** * Logs time to first fix * * @param timeToFirstFixMilliSeconds */
public void logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds) { timeToFirstFixSecStatistics.addItem((double) (timeToFirstFixMilliSeconds/1000)); return; }
Logs position accuracy
Params:
  • positionAccuracyMeters –
/** * 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
Returns:
/** * 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
Returns:GNSS Metrics
/** * 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
/** Class for storing statistics */
private class Statistics {
Resets statistics
/** Resets statistics */
public void reset() { count = 0; sum = 0.0; sumSquare = 0.0; }
Adds an item
/** Adds an item */
public void addItem(double item) { count++; sum += item; sumSquare += item * item; }
Returns number of items added
/** Returns number of items added */
public int getCount() { return count; }
Returns mean
/** Returns mean */
public double getMean() { return sum/count; }
Returns standard deviation
/** 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
/** Location failure statistics */
private Statistics locationFailureStatistics;
Time to first fix statistics
/** Time to first fix statistics */
private Statistics timeToFirstFixSecStatistics;
Position accuracy statistics
/** Position accuracy statistics */
private Statistics positionAccuracyMeterStatistics;
Top 4 average CN0 statistics
/** Top 4 average CN0 statistics */
private Statistics topFourAverageCn0Statistics;
Resets GNSS metrics
/** * 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.
Returns:PowerMetrics
/** * 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
Returns:GpsBatteryStats
/** * 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.
/** * 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
/** * 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; } } }