1 /*
2  * Copyright (C) 2013 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 package android.hardware.cts.helpers;
17 
18 import android.hardware.Sensor;
19 import android.os.Environment;
20 import android.util.Log;
21 import java.io.File;
22 import java.io.IOException;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.concurrent.TimeUnit;
28 
29 /**
30  * Set of static helper methods for CTS tests.
31  */
32 //TODO: Refactor this class into several more well defined helper classes, look at StatisticsUtils
33 public class SensorCtsHelper {
34 
35     private static final long NANOS_PER_MILLI = 1000000;
36 
37     /**
38      * Private constructor for static class.
39      */
SensorCtsHelper()40     private SensorCtsHelper() {}
41 
42     /**
43      * Get low and high percentiles values of an array
44      *
45      * @param lowPercentile Lower boundary percentile, range [0, 1]
46      * @param highPercentile Higher boundary percentile, range [0, 1]
47      *
48      * @throws IllegalArgumentException if the collection or percentiles is null or empty.
49      */
getPercentileValue( Collection<TValue> collection, float lowPecentile, float highPercentile)50     public static <TValue extends Comparable<? super TValue>> List<TValue> getPercentileValue(
51             Collection<TValue> collection, float lowPecentile, float highPercentile) {
52         validateCollection(collection);
53         if (lowPecentile > highPercentile || lowPecentile < 0 || highPercentile > 1) {
54             throw new IllegalStateException("percentile has to be in range [0, 1], and " +
55                     "lowPecentile has to be less than or equal to highPercentile");
56         }
57 
58         List<TValue> arrayCopy = new ArrayList<TValue>(collection);
59         Collections.sort(arrayCopy);
60 
61         List<TValue> percentileValues = new ArrayList<TValue>();
62         // lower percentile: rounding upwards, index range 1 .. size - 1 for percentile > 0
63         // for percentile == 0, index will be 0.
64         int lowArrayIndex = Math.min(arrayCopy.size() - 1,
65                 arrayCopy.size() - (int)(arrayCopy.size() * (1 - lowPecentile)));
66         percentileValues.add(arrayCopy.get(lowArrayIndex));
67 
68         // upper percentile: rounding downwards, index range 0 .. size - 2 for percentile < 1
69         // for percentile == 1, index will be size - 1.
70         // Also, lower bound by lowerArrayIndex to avoid low percentile value being higher than
71         // high percentile value.
72         int highArrayIndex = Math.max(lowArrayIndex, (int)(arrayCopy.size() * highPercentile - 1));
73         percentileValues.add(arrayCopy.get(highArrayIndex));
74         return percentileValues;
75     }
76 
77     /**
78      * Calculate the mean of a collection.
79      *
80      * @throws IllegalArgumentException if the collection is null or empty
81      */
getMean(Collection<TValue> collection)82     public static <TValue extends Number> double getMean(Collection<TValue> collection) {
83         validateCollection(collection);
84 
85         double sum = 0.0;
86         for(TValue value : collection) {
87             sum += value.doubleValue();
88         }
89         return sum / collection.size();
90     }
91 
92     /**
93      * Calculate the bias-corrected sample variance of a collection.
94      *
95      * @throws IllegalArgumentException if the collection is null or empty
96      */
getVariance(Collection<TValue> collection)97     public static <TValue extends Number> double getVariance(Collection<TValue> collection) {
98         validateCollection(collection);
99 
100         double mean = getMean(collection);
101         ArrayList<Double> squaredDiffs = new ArrayList<Double>();
102         for(TValue value : collection) {
103             double difference = mean - value.doubleValue();
104             squaredDiffs.add(Math.pow(difference, 2));
105         }
106 
107         double sum = 0.0;
108         for (Double value : squaredDiffs) {
109             sum += value;
110         }
111         return sum / (squaredDiffs.size() - 1);
112     }
113 
114     /**
115      * @return The (measured) sampling rate of a collection of {@link TestSensorEvent}.
116      */
getSamplingPeriodNs(List<TestSensorEvent> collection)117     public static long getSamplingPeriodNs(List<TestSensorEvent> collection) {
118         int collectionSize = collection.size();
119         if (collectionSize < 2) {
120             return 0;
121         }
122         TestSensorEvent firstEvent = collection.get(0);
123         TestSensorEvent lastEvent = collection.get(collectionSize - 1);
124         return (lastEvent.timestamp - firstEvent.timestamp) / (collectionSize - 1);
125     }
126 
127     /**
128      * Calculate the bias-corrected standard deviation of a collection.
129      *
130      * @throws IllegalArgumentException if the collection is null or empty
131      */
getStandardDeviation( Collection<TValue> collection)132     public static <TValue extends Number> double getStandardDeviation(
133             Collection<TValue> collection) {
134         return Math.sqrt(getVariance(collection));
135     }
136 
137     /**
138      * Convert a period to frequency in Hz.
139      */
getFrequency(TValue period, TimeUnit unit)140     public static <TValue extends Number> double getFrequency(TValue period, TimeUnit unit) {
141         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * period.doubleValue());
142     }
143 
144     /**
145      * Convert a frequency in Hz into a period.
146      */
getPeriod(TValue frequency, TimeUnit unit)147     public static <TValue extends Number> double getPeriod(TValue frequency, TimeUnit unit) {
148         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * frequency.doubleValue());
149     }
150 
151     /**
152      * If value lies outside the boundary limit, then return the nearer bound value.
153      * Otherwise, return the value unchanged.
154      */
clamp(TValue val, TValue min, TValue max)155     public static <TValue extends Number> double clamp(TValue val, TValue min, TValue max) {
156         return Math.min(max.doubleValue(), Math.max(min.doubleValue(), val.doubleValue()));
157     }
158 
159     /**
160      * @return The magnitude (norm) represented by the given array of values.
161      */
getMagnitude(float[] values)162     public static double getMagnitude(float[] values) {
163         float sumOfSquares = 0.0f;
164         for (float value : values) {
165             sumOfSquares += value * value;
166         }
167         double magnitude = Math.sqrt(sumOfSquares);
168         return magnitude;
169     }
170 
171     /**
172      * Helper method to sleep for a given duration.
173      */
sleep(long duration, TimeUnit timeUnit)174     public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException {
175         long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit);
176         Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
177     }
178 
179     /**
180      * Format an assertion message.
181      *
182      * @param label the verification name
183      * @param environment the environment of the test
184      *
185      * @return The formatted string
186      */
formatAssertionMessage(String label, TestSensorEnvironment environment)187     public static String formatAssertionMessage(String label, TestSensorEnvironment environment) {
188         return formatAssertionMessage(label, environment, "Failed");
189     }
190 
191     /**
192      * Format an assertion message with a custom message.
193      *
194      * @param label the verification name
195      * @param environment the environment of the test
196      * @param format the additional format string
197      * @param params the additional format params
198      *
199      * @return The formatted string
200      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String format, Object ... params)201     public static String formatAssertionMessage(
202             String label,
203             TestSensorEnvironment environment,
204             String format,
205             Object ... params) {
206         return formatAssertionMessage(label, environment, String.format(format, params));
207     }
208 
209     /**
210      * Format an assertion message.
211      *
212      * @param label the verification name
213      * @param environment the environment of the test
214      * @param extras the additional information for the assertion
215      *
216      * @return The formatted string
217      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String extras)218     public static String formatAssertionMessage(
219             String label,
220             TestSensorEnvironment environment,
221             String extras) {
222         return String.format(
223                 "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s",
224                 label,
225                 environment.getSensor().getName(),
226                 environment.getRequestedSamplingPeriodUs(),
227                 environment.getMaxReportLatencyUs(),
228                 extras);
229     }
230 
231     /**
232      * Format an array of floats.
233      *
234      * @param array the array of floats
235      *
236      * @return The formatted string
237      */
formatFloatArray(float[] array)238     public static String formatFloatArray(float[] array) {
239         StringBuilder sb = new StringBuilder();
240         if (array.length > 1) {
241             sb.append("(");
242         }
243         for (int i = 0; i < array.length; i++) {
244             sb.append(String.format("%.2f", array[i]));
245             if (i != array.length - 1) {
246                 sb.append(", ");
247             }
248         }
249         if (array.length > 1) {
250             sb.append(")");
251         }
252         return sb.toString();
253     }
254 
255     /**
256      * @return A {@link File} representing a root directory to store sensor tests data.
257      */
getSensorTestDataDirectory()258     public static File getSensorTestDataDirectory() throws IOException {
259         File dataDirectory = new File(Environment.getExternalStorageDirectory(), "sensorTests/");
260         return createDirectoryStructure(dataDirectory);
261     }
262 
263     /**
264      * Creates the directory structure for the given sensor test data sub-directory.
265      *
266      * @param subdirectory The sub-directory's name.
267      */
getSensorTestDataDirectory(String subdirectory)268     public static File getSensorTestDataDirectory(String subdirectory) throws IOException {
269         File subdirectoryFile = new File(getSensorTestDataDirectory(), subdirectory);
270         return createDirectoryStructure(subdirectoryFile);
271     }
272 
273     /**
274      * Sanitizes a string so it can be used in file names.
275      *
276      * @param value The string to sanitize.
277      * @return The sanitized string.
278      *
279      * @throws SensorTestPlatformException If the string cannot be sanitized.
280      */
sanitizeStringForFileName(String value)281     public static String sanitizeStringForFileName(String value)
282             throws SensorTestPlatformException {
283         String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_");
284         if (sanitizedValue.matches("_*")) {
285             throw new SensorTestPlatformException(
286                     "Unable to sanitize string '%s' for file name.",
287                     value);
288         }
289         return sanitizedValue;
290     }
291 
292     /**
293      * Ensures that the directory structure represented by the given {@link File} is created.
294      */
createDirectoryStructure(File directoryStructure)295     private static File createDirectoryStructure(File directoryStructure) throws IOException {
296         directoryStructure.mkdirs();
297         if (!directoryStructure.isDirectory()) {
298             throw new IOException("Unable to create directory structure for "
299                     + directoryStructure.getAbsolutePath());
300         }
301         return directoryStructure;
302     }
303 
304     /**
305      * Validate that a collection is not null or empty.
306      *
307      * @throws IllegalStateException if collection is null or empty.
308      */
validateCollection(Collection<T> collection)309     private static <T> void validateCollection(Collection<T> collection) {
310         if(collection == null || collection.size() == 0) {
311             throw new IllegalStateException("Collection cannot be null or empty");
312         }
313     }
314 
getUnitsForSensor(Sensor sensor)315     public static String getUnitsForSensor(Sensor sensor) {
316         switch(sensor.getType()) {
317             case Sensor.TYPE_ACCELEROMETER:
318                 return "m/s^2";
319             case Sensor.TYPE_MAGNETIC_FIELD:
320             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
321                 return "uT";
322             case Sensor.TYPE_GYROSCOPE:
323             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
324                 return "radians/sec";
325             case Sensor.TYPE_PRESSURE:
326                 return "hPa";
327         };
328         return "";
329     }
330 
hasResolutionRequirement(Sensor sensor, boolean hasHifiSensors)331     public static boolean hasResolutionRequirement(Sensor sensor, boolean hasHifiSensors) {
332         switch (sensor.getType()) {
333             case Sensor.TYPE_ACCELEROMETER:
334             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
335             case Sensor.TYPE_GYROSCOPE:
336             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
337             case Sensor.TYPE_MAGNETIC_FIELD:
338             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
339                 return true;
340 
341             case Sensor.TYPE_PRESSURE:
342                 // Pressure sensor only has a resolution requirement when there are HiFi sensors
343                 return hasHifiSensors;
344         }
345         return false;
346     }
347 
getRequiredResolutionForSensor(Sensor sensor)348     public static float getRequiredResolutionForSensor(Sensor sensor) {
349         switch (sensor.getType()) {
350             case Sensor.TYPE_ACCELEROMETER:
351             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
352             case Sensor.TYPE_GYROSCOPE:
353             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
354                 // Accelerometer and gyroscope must have at least 12 bits
355                 // of resolution. The maximum resolution calculation uses
356                 // slightly more than twice the maximum range because
357                 //   1) the sensor must be able to report values from
358                 //      [-maxRange, maxRange] without saturating
359                 //   2) to allow for slight rounding errors
360                 return (float)(2.001f * sensor.getMaximumRange() / Math.pow(2, 12));
361             case Sensor.TYPE_MAGNETIC_FIELD:
362             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
363                 // Magnetometer must have a resolution equal to or denser
364                 // than 0.6 uT
365                 return 0.6f;
366             case Sensor.TYPE_PRESSURE:
367                 // Pressure sensor must have at least 80 LSB / hPa which is
368                 // equivalent to 0.0125 hPa / LSB. Allow for a small margin of
369                 // error due to rounding errors.
370                 return 1.01f * (1.0f / 80.0f);
371         }
372         return 0.0f;
373     }
374 
sensorTypeShortString(int type)375     public static String sensorTypeShortString(int type) {
376         switch (type) {
377             case Sensor.TYPE_ACCELEROMETER:
378                 return "Accel";
379             case Sensor.TYPE_GYROSCOPE:
380                 return "Gyro";
381             case Sensor.TYPE_MAGNETIC_FIELD:
382                 return "Mag";
383             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
384                 return "UncalAccel";
385             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
386                 return "UncalGyro";
387             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
388                 return "UncalMag";
389             default:
390                 return "Type_" + type;
391         }
392     }
393 
394     public static class TestResultCollector {
395         private List<AssertionError> mErrorList = new ArrayList<>();
396         private List<String> mErrorStringList = new ArrayList<>();
397         private String mTestName;
398         private String mTag;
399 
TestResultCollector()400         public TestResultCollector() {
401             this("Test");
402         }
403 
TestResultCollector(String test)404         public TestResultCollector(String test) {
405             this(test, "SensorCtsTest");
406         }
407 
TestResultCollector(String test, String tag)408         public TestResultCollector(String test, String tag) {
409             mTestName = test;
410             mTag = tag;
411         }
412 
perform(Runnable r)413         public void perform(Runnable r) {
414             perform(r, "");
415         }
416 
perform(Runnable r, String s)417         public void perform(Runnable r, String s) {
418             try {
419                 Log.d(mTag, mTestName + " running " + (s.isEmpty() ? "..." : s));
420                 r.run();
421             } catch (AssertionError e) {
422                 mErrorList.add(e);
423                 mErrorStringList.add(s);
424                 Log.e(mTag, mTestName + " error: " + e.getMessage());
425             }
426         }
427 
judge()428         public void judge() throws AssertionError {
429             if (mErrorList.isEmpty() && mErrorStringList.isEmpty()) {
430                 return;
431             }
432 
433             if (mErrorList.size() != mErrorStringList.size()) {
434                 throw new IllegalStateException("Mismatch error and error message");
435             }
436 
437             StringBuffer buf = new StringBuffer();
438             for (int i = 0; i < mErrorList.size(); ++i) {
439                 buf.append("Test (").append(mErrorStringList.get(i)).append(") - Error: ")
440                     .append(mErrorList.get(i).getMessage()).append("; ");
441             }
442             throw new AssertionError(buf.toString());
443         }
444     }
445 
bytesToHex(byte[] bytes, int offset, int length)446     public static String bytesToHex(byte[] bytes, int offset, int length) {
447         if (offset == -1) {
448             offset = 0;
449         }
450 
451         if (length == -1) {
452             length = bytes.length;
453         }
454 
455         final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
456         char[] hexChars = new char[length * 3];
457         int v;
458         for (int i = 0; i < length; i++) {
459             v = bytes[offset + i] & 0xFF;
460             hexChars[i * 3] = hexArray[v >>> 4];
461             hexChars[i * 3 + 1] = hexArray[v & 0x0F];
462             hexChars[i * 3 + 2] = ' ';
463         }
464         return new String(hexChars);
465     }
466 }
467