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