1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.location.gnssmetrics; 18 19 import android.os.SystemClock; 20 import android.os.connectivity.GpsBatteryStats; 21 22 import android.text.format.DateUtils; 23 import android.util.Base64; 24 import android.util.Log; 25 import android.util.TimeUtils; 26 27 import java.util.Arrays; 28 29 import com.android.internal.app.IBatteryStats; 30 import com.android.internal.location.nano.GnssLogsProto.GnssLog; 31 import com.android.internal.location.nano.GnssLogsProto.PowerMetrics; 32 33 /** 34 * GnssMetrics: Is used for logging GNSS metrics 35 * @hide 36 */ 37 public class GnssMetrics { 38 39 private static final String TAG = GnssMetrics.class.getSimpleName(); 40 41 /* Constant which indicates GPS signal quality is poor */ 42 public static final int GPS_SIGNAL_QUALITY_POOR = 0; 43 44 /* Constant which indicates GPS signal quality is good */ 45 public static final int GPS_SIGNAL_QUALITY_GOOD = 1; 46 47 /* Number of GPS signal quality levels */ 48 public static final int NUM_GPS_SIGNAL_QUALITY_LEVELS = GPS_SIGNAL_QUALITY_GOOD + 1; 49 50 /** Default time between location fixes (in millisecs) */ 51 private static final int DEFAULT_TIME_BETWEEN_FIXES_MILLISECS = 1000; 52 53 /* The time since boot when logging started */ 54 private String logStartInElapsedRealTime; 55 56 /* GNSS power metrics */ 57 private GnssPowerMetrics mGnssPowerMetrics; 58 59 /** Constructor */ GnssMetrics(IBatteryStats stats)60 public GnssMetrics(IBatteryStats stats) { 61 mGnssPowerMetrics = new GnssPowerMetrics(stats); 62 locationFailureStatistics = new Statistics(); 63 timeToFirstFixSecStatistics = new Statistics(); 64 positionAccuracyMeterStatistics = new Statistics(); 65 topFourAverageCn0Statistics = new Statistics(); 66 reset(); 67 } 68 69 /** 70 * Logs the status of a location report received from the HAL 71 * 72 * @param isSuccessful 73 */ logReceivedLocationStatus(boolean isSuccessful)74 public void logReceivedLocationStatus(boolean isSuccessful) { 75 if (!isSuccessful) { 76 locationFailureStatistics.addItem(1.0); 77 return; 78 } 79 locationFailureStatistics.addItem(0.0); 80 return; 81 } 82 83 /** 84 * Logs missed reports 85 * 86 * @param desiredTimeBetweenFixesMilliSeconds 87 * @param actualTimeBetweenFixesMilliSeconds 88 */ logMissedReports(int desiredTimeBetweenFixesMilliSeconds, int actualTimeBetweenFixesMilliSeconds)89 public void logMissedReports(int desiredTimeBetweenFixesMilliSeconds, 90 int actualTimeBetweenFixesMilliSeconds) { 91 int numReportMissed = (actualTimeBetweenFixesMilliSeconds / 92 Math.max(DEFAULT_TIME_BETWEEN_FIXES_MILLISECS, desiredTimeBetweenFixesMilliSeconds)) - 1; 93 if (numReportMissed > 0) { 94 for (int i = 0; i < numReportMissed; i++) { 95 locationFailureStatistics.addItem(1.0); 96 } 97 } 98 return; 99 } 100 101 /** 102 * Logs time to first fix 103 * 104 * @param timeToFirstFixMilliSeconds 105 */ logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds)106 public void logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds) { 107 timeToFirstFixSecStatistics.addItem((double) (timeToFirstFixMilliSeconds/1000)); 108 return; 109 } 110 111 /** 112 * Logs position accuracy 113 * 114 * @param positionAccuracyMeters 115 */ logPositionAccuracyMeters(float positionAccuracyMeters)116 public void logPositionAccuracyMeters(float positionAccuracyMeters) { 117 positionAccuracyMeterStatistics.addItem((double) positionAccuracyMeters); 118 return; 119 } 120 121 /* 122 * Logs CN0 when at least 4 SVs are available 123 * 124 */ logCn0(float[] cn0s, int numSv)125 public void logCn0(float[] cn0s, int numSv) { 126 if (numSv == 0 || cn0s == null || cn0s.length == 0 || cn0s.length < numSv) { 127 if (numSv == 0) { 128 mGnssPowerMetrics.reportSignalQuality(null, 0); 129 } 130 return; 131 } 132 float[] cn0Array = Arrays.copyOf(cn0s, numSv); 133 Arrays.sort(cn0Array); 134 mGnssPowerMetrics.reportSignalQuality(cn0Array, numSv); 135 if (numSv < 4) { 136 return; 137 } 138 if (cn0Array[numSv - 4] > 0.0) { 139 double top4AvgCn0 = 0.0; 140 for (int i = numSv - 4; i < numSv; i++) { 141 top4AvgCn0 += (double) cn0Array[i]; 142 } 143 top4AvgCn0 /= 4; 144 topFourAverageCn0Statistics.addItem(top4AvgCn0); 145 } 146 return; 147 } 148 149 /** 150 * Dumps GNSS metrics as a proto string 151 * @return 152 */ dumpGnssMetricsAsProtoString()153 public String dumpGnssMetricsAsProtoString() { 154 GnssLog msg = new GnssLog(); 155 if (locationFailureStatistics.getCount() > 0) { 156 msg.numLocationReportProcessed = locationFailureStatistics.getCount(); 157 msg.percentageLocationFailure = (int) (100.0 * locationFailureStatistics.getMean()); 158 } 159 if (timeToFirstFixSecStatistics.getCount() > 0) { 160 msg.numTimeToFirstFixProcessed = timeToFirstFixSecStatistics.getCount(); 161 msg.meanTimeToFirstFixSecs = (int) timeToFirstFixSecStatistics.getMean(); 162 msg.standardDeviationTimeToFirstFixSecs 163 = (int) timeToFirstFixSecStatistics.getStandardDeviation(); 164 } 165 if (positionAccuracyMeterStatistics.getCount() > 0) { 166 msg.numPositionAccuracyProcessed = positionAccuracyMeterStatistics.getCount(); 167 msg.meanPositionAccuracyMeters = (int) positionAccuracyMeterStatistics.getMean(); 168 msg.standardDeviationPositionAccuracyMeters 169 = (int) positionAccuracyMeterStatistics.getStandardDeviation(); 170 } 171 if (topFourAverageCn0Statistics.getCount() > 0) { 172 msg.numTopFourAverageCn0Processed = topFourAverageCn0Statistics.getCount(); 173 msg.meanTopFourAverageCn0DbHz = topFourAverageCn0Statistics.getMean(); 174 msg.standardDeviationTopFourAverageCn0DbHz 175 = topFourAverageCn0Statistics.getStandardDeviation(); 176 } 177 msg.powerMetrics = mGnssPowerMetrics.buildProto(); 178 String s = Base64.encodeToString(GnssLog.toByteArray(msg), Base64.DEFAULT); 179 reset(); 180 return s; 181 } 182 183 /** 184 * Dumps GNSS Metrics as text 185 * 186 * @return GNSS Metrics 187 */ dumpGnssMetricsAsText()188 public String dumpGnssMetricsAsText() { 189 StringBuilder s = new StringBuilder(); 190 s.append("GNSS_KPI_START").append('\n'); 191 s.append(" KPI logging start time: ").append(logStartInElapsedRealTime).append("\n"); 192 s.append(" KPI logging end time: "); 193 TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s); 194 s.append("\n"); 195 s.append(" Number of location reports: ").append( 196 locationFailureStatistics.getCount()).append("\n"); 197 if (locationFailureStatistics.getCount() > 0) { 198 s.append(" Percentage location failure: ").append( 199 100.0 * locationFailureStatistics.getMean()).append("\n"); 200 } 201 s.append(" Number of TTFF reports: ").append( 202 timeToFirstFixSecStatistics.getCount()).append("\n"); 203 if (timeToFirstFixSecStatistics.getCount() > 0) { 204 s.append(" TTFF mean (sec): ").append(timeToFirstFixSecStatistics.getMean()).append("\n"); 205 s.append(" TTFF standard deviation (sec): ").append( 206 timeToFirstFixSecStatistics.getStandardDeviation()).append("\n"); 207 } 208 s.append(" Number of position accuracy reports: ").append( 209 positionAccuracyMeterStatistics.getCount()).append("\n"); 210 if (positionAccuracyMeterStatistics.getCount() > 0) { 211 s.append(" Position accuracy mean (m): ").append( 212 positionAccuracyMeterStatistics.getMean()).append("\n"); 213 s.append(" Position accuracy standard deviation (m): ").append( 214 positionAccuracyMeterStatistics.getStandardDeviation()).append("\n"); 215 } 216 s.append(" Number of CN0 reports: ").append( 217 topFourAverageCn0Statistics.getCount()).append("\n"); 218 if (topFourAverageCn0Statistics.getCount() > 0) { 219 s.append(" Top 4 Avg CN0 mean (dB-Hz): ").append( 220 topFourAverageCn0Statistics.getMean()).append("\n"); 221 s.append(" Top 4 Avg CN0 standard deviation (dB-Hz): ").append( 222 topFourAverageCn0Statistics.getStandardDeviation()).append("\n"); 223 } 224 s.append("GNSS_KPI_END").append("\n"); 225 GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats(); 226 if (stats != null) { 227 s.append("Power Metrics").append("\n"); 228 s.append(" Time on battery (min): " 229 + stats.getLoggingDurationMs() / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n"); 230 long[] t = stats.getTimeInGpsSignalQualityLevel(); 231 if (t != null && t.length == NUM_GPS_SIGNAL_QUALITY_LEVELS) { 232 s.append(" Amount of time (while on battery) Top 4 Avg CN0 > " + 233 Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) + 234 " dB-Hz (min): ").append(t[1] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n"); 235 s.append(" Amount of time (while on battery) Top 4 Avg CN0 <= " + 236 Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) + 237 " dB-Hz (min): ").append(t[0] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n"); 238 } 239 s.append(" Energy consumed while on battery (mAh): ").append( 240 stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS)).append("\n"); 241 } 242 return s.toString(); 243 } 244 245 /** Class for storing statistics */ 246 private class Statistics { 247 248 /** Resets statistics */ reset()249 public void reset() { 250 count = 0; 251 sum = 0.0; 252 sumSquare = 0.0; 253 } 254 255 /** Adds an item */ addItem(double item)256 public void addItem(double item) { 257 count++; 258 sum += item; 259 sumSquare += item * item; 260 } 261 262 /** Returns number of items added */ getCount()263 public int getCount() { 264 return count; 265 } 266 267 /** Returns mean */ getMean()268 public double getMean() { 269 return sum/count; 270 } 271 272 /** Returns standard deviation */ getStandardDeviation()273 public double getStandardDeviation() { 274 double m = sum/count; 275 m = m * m; 276 double v = sumSquare/count; 277 if (v > m) { 278 return Math.sqrt(v - m); 279 } 280 return 0; 281 } 282 283 private int count; 284 private double sum; 285 private double sumSquare; 286 } 287 288 /** Location failure statistics */ 289 private Statistics locationFailureStatistics; 290 291 /** Time to first fix statistics */ 292 private Statistics timeToFirstFixSecStatistics; 293 294 /** Position accuracy statistics */ 295 private Statistics positionAccuracyMeterStatistics; 296 297 /** Top 4 average CN0 statistics */ 298 private Statistics topFourAverageCn0Statistics; 299 300 /** 301 * Resets GNSS metrics 302 */ reset()303 private void reset() { 304 StringBuilder s = new StringBuilder(); 305 TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s); 306 logStartInElapsedRealTime = s.toString(); 307 locationFailureStatistics.reset(); 308 timeToFirstFixSecStatistics.reset(); 309 positionAccuracyMeterStatistics.reset(); 310 topFourAverageCn0Statistics.reset(); 311 return; 312 } 313 314 /* Class for handling GNSS power related metrics */ 315 private class GnssPowerMetrics { 316 317 /* Threshold for Top Four Average CN0 below which GNSS signal quality is declared poor */ 318 public static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0; 319 320 /* Minimum change in Top Four Average CN0 needed to trigger a report */ 321 private static final double REPORTING_THRESHOLD_DB_HZ = 1.0; 322 323 /* BatteryStats API */ 324 private final IBatteryStats mBatteryStats; 325 326 /* Last reported Top Four Average CN0 */ 327 private double mLastAverageCn0; 328 GnssPowerMetrics(IBatteryStats stats)329 public GnssPowerMetrics(IBatteryStats stats) { 330 mBatteryStats = stats; 331 // Used to initialize the variable to a very small value (unachievable in practice) so that 332 // the first CNO report will trigger an update to BatteryStats 333 mLastAverageCn0 = -100.0; 334 } 335 336 /** 337 * Builds power metrics proto buf. This is included in the gnss proto buf. 338 * @return PowerMetrics 339 */ buildProto()340 public PowerMetrics buildProto() { 341 PowerMetrics p = new PowerMetrics(); 342 GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats(); 343 if (stats != null) { 344 p.loggingDurationMs = stats.getLoggingDurationMs(); 345 p.energyConsumedMah = stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS); 346 long[] t = stats.getTimeInGpsSignalQualityLevel(); 347 p.timeInSignalQualityLevelMs = new long[t.length]; 348 for (int i = 0; i < t.length; i++) { 349 p.timeInSignalQualityLevelMs[i] = t[i]; 350 } 351 } 352 return p; 353 } 354 355 /** 356 * Returns the GPS power stats 357 * @return GpsBatteryStats 358 */ getGpsBatteryStats()359 public GpsBatteryStats getGpsBatteryStats() { 360 try { 361 return mBatteryStats.getGpsBatteryStats(); 362 } catch (Exception e) { 363 Log.w(TAG, "Exception", e); 364 return null; 365 } 366 } 367 368 /** 369 * Reports signal quality to BatteryStats. Signal quality is based on Top four average CN0. If 370 * the number of SVs seen is less than 4, then signal quality is the average CN0. 371 * Changes are reported only if the average CN0 changes by more than REPORTING_THRESHOLD_DB_HZ. 372 */ reportSignalQuality(float[] ascendingCN0Array, int numSv)373 public void reportSignalQuality(float[] ascendingCN0Array, int numSv) { 374 double avgCn0 = 0.0; 375 if (numSv > 0) { 376 for (int i = Math.max(0, numSv - 4); i < numSv; i++) { 377 avgCn0 += (double) ascendingCN0Array[i]; 378 } 379 avgCn0 /= Math.min(numSv, 4); 380 } 381 if (Math.abs(avgCn0 - mLastAverageCn0) < REPORTING_THRESHOLD_DB_HZ) { 382 return; 383 } 384 try { 385 mBatteryStats.noteGpsSignalQuality(getSignalLevel(avgCn0)); 386 mLastAverageCn0 = avgCn0; 387 } catch (Exception e) { 388 Log.w(TAG, "Exception", e); 389 } 390 return; 391 } 392 393 /** 394 * Obtains signal level based on CN0 395 */ getSignalLevel(double cn0)396 private int getSignalLevel(double cn0) { 397 if (cn0 > POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) { 398 return GnssMetrics.GPS_SIGNAL_QUALITY_GOOD; 399 } 400 return GnssMetrics.GPS_SIGNAL_QUALITY_POOR; 401 } 402 } 403 } 404