/* * 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.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(); 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"); } 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; } } }