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
17package com.android.internal.location.gnssmetrics;
18
19import android.os.SystemClock;
20import android.os.connectivity.GpsBatteryStats;
21
22import android.text.format.DateUtils;
23import android.util.Base64;
24import android.util.Log;
25import android.util.TimeUtils;
26
27import java.util.Arrays;
28
29import com.android.internal.app.IBatteryStats;
30import com.android.internal.location.nano.GnssLogsProto.GnssLog;
31import com.android.internal.location.nano.GnssLogsProto.PowerMetrics;
32
33/**
34 * GnssMetrics: Is used for logging GNSS metrics
35 * @hide
36 */
37public 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 */
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   */
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   */
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   */
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   */
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  */
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   */
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   */
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 */
249    public void reset() {
250      count = 0;
251      sum = 0.0;
252      sumSquare = 0.0;
253    }
254
255    /** Adds an item */
256    public void addItem(double item) {
257      count++;
258      sum += item;
259      sumSquare += item * item;
260    }
261
262    /** Returns number of items added */
263    public int getCount() {
264      return count;
265    }
266
267    /** Returns mean */
268    public double getMean() {
269      return sum/count;
270    }
271
272    /** Returns standard deviation */
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   */
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
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     */
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     */
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     */
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     */
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