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