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      * If value lies outside the boundary limit, then return the nearer bound value.
145      * Otherwise, return the value unchanged.
146      */
clamp(TValue val, TValue min, TValue max)147     public static <TValue extends Number> double clamp(TValue val, TValue min, TValue max) {
148         return Math.min(max.doubleValue(), Math.max(min.doubleValue(), val.doubleValue()));
149     }
150 
151     /**
152      * @return The magnitude (norm) represented by the given array of values.
153      */
getMagnitude(float[] values)154     public static double getMagnitude(float[] values) {
155         float sumOfSquares = 0.0f;
156         for (float value : values) {
157             sumOfSquares += value * value;
158         }
159         double magnitude = Math.sqrt(sumOfSquares);
160         return magnitude;
161     }
162 
163     /**
164      * Helper method to sleep for a given duration.
165      */
sleep(long duration, TimeUnit timeUnit)166     public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException {
167         long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit);
168         Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
169     }
170 
171     /**
172      * Format an assertion message.
173      *
174      * @param label the verification name
175      * @param environment the environment of the test
176      *
177      * @return The formatted string
178      */
formatAssertionMessage(String label, TestSensorEnvironment environment)179     public static String formatAssertionMessage(String label, TestSensorEnvironment environment) {
180         return formatAssertionMessage(label, environment, "Failed");
181     }
182 
183     /**
184      * Format an assertion message with a custom message.
185      *
186      * @param label the verification name
187      * @param environment the environment of the test
188      * @param format the additional format string
189      * @param params the additional format params
190      *
191      * @return The formatted string
192      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String format, Object ... params)193     public static String formatAssertionMessage(
194             String label,
195             TestSensorEnvironment environment,
196             String format,
197             Object ... params) {
198         return formatAssertionMessage(label, environment, String.format(format, params));
199     }
200 
201     /**
202      * Format an assertion message.
203      *
204      * @param label the verification name
205      * @param environment the environment of the test
206      * @param extras the additional information for the assertion
207      *
208      * @return The formatted string
209      */
formatAssertionMessage( String label, TestSensorEnvironment environment, String extras)210     public static String formatAssertionMessage(
211             String label,
212             TestSensorEnvironment environment,
213             String extras) {
214         return String.format(
215                 "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s",
216                 label,
217                 environment.getSensor().getName(),
218                 environment.getRequestedSamplingPeriodUs(),
219                 environment.getMaxReportLatencyUs(),
220                 extras);
221     }
222 
223     /**
224      * @return A {@link File} representing a root directory to store sensor tests data.
225      */
getSensorTestDataDirectory()226     public static File getSensorTestDataDirectory() throws IOException {
227         File dataDirectory = new File(System.getenv("EXTERNAL_STORAGE"), "sensorTests/");
228         return createDirectoryStructure(dataDirectory);
229     }
230 
231     /**
232      * Creates the directory structure for the given sensor test data sub-directory.
233      *
234      * @param subdirectory The sub-directory's name.
235      */
getSensorTestDataDirectory(String subdirectory)236     public static File getSensorTestDataDirectory(String subdirectory) throws IOException {
237         File subdirectoryFile = new File(getSensorTestDataDirectory(), subdirectory);
238         return createDirectoryStructure(subdirectoryFile);
239     }
240 
241     /**
242      * Sanitizes a string so it can be used in file names.
243      *
244      * @param value The string to sanitize.
245      * @return The sanitized string.
246      *
247      * @throws SensorTestPlatformException If the string cannot be sanitized.
248      */
sanitizeStringForFileName(String value)249     public static String sanitizeStringForFileName(String value)
250             throws SensorTestPlatformException {
251         String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_");
252         if (sanitizedValue.matches("_*")) {
253             throw new SensorTestPlatformException(
254                     "Unable to sanitize string '%s' for file name.",
255                     value);
256         }
257         return sanitizedValue;
258     }
259 
260     /**
261      * Ensures that the directory structure represented by the given {@link File} is created.
262      */
createDirectoryStructure(File directoryStructure)263     private static File createDirectoryStructure(File directoryStructure) throws IOException {
264         directoryStructure.mkdirs();
265         if (!directoryStructure.isDirectory()) {
266             throw new IOException("Unable to create directory structure for "
267                     + directoryStructure.getAbsolutePath());
268         }
269         return directoryStructure;
270     }
271 
272     /**
273      * Validate that a collection is not null or empty.
274      *
275      * @throws IllegalStateException if collection is null or empty.
276      */
validateCollection(Collection<T> collection)277     private static <T> void validateCollection(Collection<T> collection) {
278         if(collection == null || collection.size() == 0) {
279             throw new IllegalStateException("Collection cannot be null or empty");
280         }
281     }
282 
getUnitsForSensor(Sensor sensor)283     public static String getUnitsForSensor(Sensor sensor) {
284         switch(sensor.getType()) {
285             case Sensor.TYPE_ACCELEROMETER:
286                 return "m/s^2";
287             case Sensor.TYPE_MAGNETIC_FIELD:
288             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
289                 return "uT";
290             case Sensor.TYPE_GYROSCOPE:
291             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
292                 return "radians/sec";
293             case Sensor.TYPE_PRESSURE:
294                 return "hPa";
295         };
296         return "";
297     }
298 }
299