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 java.io.File;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.concurrent.TimeUnit;
26 
27 /**
28  * Set of static helper methods for CTS tests.
29  */
30 //TODO: Refactor this class into several more well defined helper classes, look at StatisticsUtils
31 public class SensorCtsHelper {
32 
33     private static final long NANOS_PER_MILLI = 1000000;
34 
35     /**
36      * Private constructor for static class.
37      */
SensorCtsHelper()38     private SensorCtsHelper() {}
39 
40     /**
41      * Get percentiles using nearest rank algorithm.
42      *
43      * @param percentiles List of percentiles interested. Its range is 0 to 1, instead of in %.
44      *        The value will be internally bounded.
45      *
46      * @throws IllegalArgumentException if the collection or percentiles is null or empty
47      */
getPercentileValue( Collection<TValue> collection, float[] percentiles)48     public static <TValue extends Comparable<? super TValue>> List<TValue> getPercentileValue(
49             Collection<TValue> collection, float[] percentiles) {
50         validateCollection(collection);
51         if(percentiles == null || percentiles.length == 0) {
52             throw new IllegalStateException("percentiles cannot be null or empty");
53         }
54 
55         List<TValue> arrayCopy = new ArrayList<TValue>(collection);
56         Collections.sort(arrayCopy);
57 
58         List<TValue> percentileValues = new ArrayList<TValue>();
59         for (float p : percentiles) {
60             // zero-based array index
61             int arrayIndex = (int) Math.round(arrayCopy.size() * p - .5f);
62             // bound the index to avoid out of range error
63             arrayIndex = Math.min(Math.max(arrayIndex, 0), arrayCopy.size() - 1);
64             percentileValues.add(arrayCopy.get(arrayIndex));
65         }
66         return percentileValues;
67     }
68 
69     /**
70      * Calculate the mean of a collection.
71      *
72      * @throws IllegalArgumentException if the collection is null or empty
73      */
getMean(Collection<TValue> collection)74     public static <TValue extends Number> double getMean(Collection<TValue> collection) {
75         validateCollection(collection);
76 
77         double sum = 0.0;
78         for(TValue value : collection) {
79             sum += value.doubleValue();
80         }
81         return sum / collection.size();
82     }
83 
84     /**
85      * Calculate the bias-corrected sample variance of a collection.
86      *
87      * @throws IllegalArgumentException if the collection is null or empty
88      */
getVariance(Collection<TValue> collection)89     public static <TValue extends Number> double getVariance(Collection<TValue> collection) {
90         validateCollection(collection);
91 
92         double mean = getMean(collection);
93         ArrayList<Double> squaredDiffs = new ArrayList<Double>();
94         for(TValue value : collection) {
95             double difference = mean - value.doubleValue();
96             squaredDiffs.add(Math.pow(difference, 2));
97         }
98 
99         double sum = 0.0;
100         for (Double value : squaredDiffs) {
101             sum += value;
102         }
103         return sum / (squaredDiffs.size() - 1);
104     }
105 
106     /**
107      * @return The (measured) sampling rate of a collection of {@link TestSensorEvent}.
108      */
getSamplingPeriodNs(List<TestSensorEvent> collection)109     public static long getSamplingPeriodNs(List<TestSensorEvent> collection) {
110         int collectionSize = collection.size();
111         if (collectionSize < 2) {
112             return 0;
113         }
114         TestSensorEvent firstEvent = collection.get(0);
115         TestSensorEvent lastEvent = collection.get(collectionSize - 1);
116         return (lastEvent.timestamp - firstEvent.timestamp) / (collectionSize - 1);
117     }
118 
119     /**
120      * Calculate the bias-corrected standard deviation of a collection.
121      *
122      * @throws IllegalArgumentException if the collection is null or empty
123      */
getStandardDeviation( Collection<TValue> collection)124     public static <TValue extends Number> double getStandardDeviation(
125             Collection<TValue> collection) {
126         return Math.sqrt(getVariance(collection));
127     }
128 
129     /**
130      * Convert a period to frequency in Hz.
131      */
getFrequency(TValue period, TimeUnit unit)132     public static <TValue extends Number> double getFrequency(TValue period, TimeUnit unit) {
133         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * period.doubleValue());
134     }
135 
136     /**
137      * Convert a frequency in Hz into a period.
138      */
getPeriod(TValue frequency, TimeUnit unit)139     public static <TValue extends Number> double getPeriod(TValue frequency, TimeUnit unit) {
140         return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * frequency.doubleValue());
141     }
142 
143     /**
144      * @return The magnitude (norm) represented by the given array of values.
145      */
getMagnitude(float[] values)146     public static double getMagnitude(float[] values) {
147         float sumOfSquares = 0.0f;
148         for (float value : values) {
149             sumOfSquares += value * value;
150         }
151         double magnitude = Math.sqrt(sumOfSquares);
152         return magnitude;
153     }
154 
155     /**
156      * Helper method to sleep for a given duration.
157      */
sleep(long duration, TimeUnit timeUnit)158     public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException {
159         long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit);
160         Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
161     }
162 
163     /**
164      * Format an assertion message.
165      *
166      * @param label the verification name
167      * @param environment the environment of the test
168      *
169      * @return The formatted string
170      */
formatAssertionMessage(String label, TestSensorEnvironment environment)171     public static String formatAssertionMessage(String label, TestSensorEnvironment environment) {
172         return formatAssertionMessage(label, environment, "Failed");
173     }
174 
175     /**
176      * Format an assertion message with a custom message.
177      *
178      * @param label the verification name
179      * @param environment the environment of the test
180      * @param format the additional format string
181      * @param params the additional format params
182      *
183      * @return The formatted string
184      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String format, Object ... params)185     public static String formatAssertionMessage(
186             String label,
187             TestSensorEnvironment environment,
188             String format,
189             Object ... params) {
190         return formatAssertionMessage(label, environment, String.format(format, params));
191     }
192 
193     /**
194      * Format an assertion message.
195      *
196      * @param label the verification name
197      * @param environment the environment of the test
198      * @param extras the additional information for the assertion
199      *
200      * @return The formatted string
201      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String extras)202     public static String formatAssertionMessage(
203             String label,
204             TestSensorEnvironment environment,
205             String extras) {
206         return String.format(
207                 "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s",
208                 label,
209                 environment.getSensor().getName(),
210                 environment.getRequestedSamplingPeriodUs(),
211                 environment.getMaxReportLatencyUs(),
212                 extras);
213     }
214 
215     /**
216      * @return A {@link File} representing a root directory to store sensor tests data.
217      */
getSensorTestDataDirectory()218     public static File getSensorTestDataDirectory() throws IOException {
219         File dataDirectory = new File(System.getenv("EXTERNAL_STORAGE"), "sensorTests/");
220         return createDirectoryStructure(dataDirectory);
221     }
222 
223     /**
224      * Creates the directory structure for the given sensor test data sub-directory.
225      *
226      * @param subdirectory The sub-directory's name.
227      */
getSensorTestDataDirectory(String subdirectory)228     public static File getSensorTestDataDirectory(String subdirectory) throws IOException {
229         File subdirectoryFile = new File(getSensorTestDataDirectory(), subdirectory);
230         return createDirectoryStructure(subdirectoryFile);
231     }
232 
233     /**
234      * Sanitizes a string so it can be used in file names.
235      *
236      * @param value The string to sanitize.
237      * @return The sanitized string.
238      *
239      * @throws SensorTestPlatformException If the string cannot be sanitized.
240      */
sanitizeStringForFileName(String value)241     public static String sanitizeStringForFileName(String value)
242             throws SensorTestPlatformException {
243         String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_");
244         if (sanitizedValue.matches("_*")) {
245             throw new SensorTestPlatformException(
246                     "Unable to sanitize string '%s' for file name.",
247                     value);
248         }
249         return sanitizedValue;
250     }
251 
252     /**
253      * Ensures that the directory structure represented by the given {@link File} is created.
254      */
createDirectoryStructure(File directoryStructure)255     private static File createDirectoryStructure(File directoryStructure) throws IOException {
256         directoryStructure.mkdirs();
257         if (!directoryStructure.isDirectory()) {
258             throw new IOException("Unable to create directory structure for "
259                     + directoryStructure.getAbsolutePath());
260         }
261         return directoryStructure;
262     }
263 
264     /**
265      * Validate that a collection is not null or empty.
266      *
267      * @throws IllegalStateException if collection is null or empty.
268      */
validateCollection(Collection<T> collection)269     private static <T> void validateCollection(Collection<T> collection) {
270         if(collection == null || collection.size() == 0) {
271             throw new IllegalStateException("Collection cannot be null or empty");
272         }
273     }
274 
getUnitsForSensor(Sensor sensor)275     public static String getUnitsForSensor(Sensor sensor) {
276         switch(sensor.getType()) {
277             case Sensor.TYPE_ACCELEROMETER:
278                 return "m/s^2";
279             case Sensor.TYPE_MAGNETIC_FIELD:
280             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
281                 return "uT";
282             case Sensor.TYPE_GYROSCOPE:
283             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
284                 return "radians/sec";
285             case Sensor.TYPE_PRESSURE:
286                 return "hPa";
287         };
288         return "";
289     }
290 }
291