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("%.8f", 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             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
325             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
326                 return "m/s^2";
327             case Sensor.TYPE_MAGNETIC_FIELD:
328             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
329                 return "uT";
330             case Sensor.TYPE_GYROSCOPE:
331             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
332             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
333             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
334                 return "radians/sec";
335             case Sensor.TYPE_PRESSURE:
336                 return "hPa";
337         };
338         return "";
339     }
340 
hasMaxResolutionRequirement(Sensor sensor, boolean hasHifiSensors)341     public static boolean hasMaxResolutionRequirement(Sensor sensor, boolean hasHifiSensors) {
342         switch (sensor.getType()) {
343             case Sensor.TYPE_ACCELEROMETER:
344             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
345             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
346             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
347             case Sensor.TYPE_GYROSCOPE:
348             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
349             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
350             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
351             case Sensor.TYPE_MAGNETIC_FIELD:
352             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
353             case Sensor.TYPE_HINGE_ANGLE:
354             case Sensor.TYPE_PROXIMITY:
355             case Sensor.TYPE_SIGNIFICANT_MOTION:
356             case Sensor.TYPE_STEP_DETECTOR:
357             case Sensor.TYPE_STEP_COUNTER:
358             case Sensor.TYPE_HEART_RATE:
359             case Sensor.TYPE_STATIONARY_DETECT:
360             case Sensor.TYPE_MOTION_DETECT:
361             case Sensor.TYPE_LOW_LATENCY_OFFBODY_DETECT:
362                 return true;
363 
364             case Sensor.TYPE_PRESSURE:
365                 // Pressure sensor only has a resolution requirement when there are HiFi sensors
366                 return hasHifiSensors;
367         }
368         return false;
369     }
370 
getRequiredMaxResolutionForSensor(Sensor sensor)371     public static float getRequiredMaxResolutionForSensor(Sensor sensor) {
372         switch (sensor.getType()) {
373             case Sensor.TYPE_ACCELEROMETER:
374             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
375             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
376             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
377             case Sensor.TYPE_GYROSCOPE:
378             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
379             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
380             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
381                 // Accelerometer and gyroscope must have at least 12 bits
382                 // of resolution. The maximum resolution calculation uses
383                 // slightly more than twice the maximum range because
384                 //   1) the sensor must be able to report values from
385                 //      [-maxRange, maxRange] without saturating
386                 //   2) to allow for slight rounding errors
387                 return (float)(2.001f * sensor.getMaximumRange() / Math.pow(2, 12));
388             case Sensor.TYPE_MAGNETIC_FIELD:
389             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
390                 // Magnetometer must have a resolution equal to or denser
391                 // than 0.6 uT
392                 return 0.6f;
393             case Sensor.TYPE_PRESSURE:
394                 // Pressure sensor must have at least 80 LSB / hPa which is
395                 // equivalent to 0.0125 hPa / LSB. Allow for a small margin of
396                 // error due to rounding errors.
397                 return 1.01f * (1.0f / 80.0f);
398             case Sensor.TYPE_HINGE_ANGLE:
399                 // Hinge angle sensor must have a resolution the same or smaller
400                 // than 360 degrees.
401                 return 360f;
402             case Sensor.TYPE_PROXIMITY:
403                 // Binary prox sensors must have a resolution of 5, but it's not
404                 // expected / recommended that prox sensors use higher than
405                 // this.
406                 return 5f;
407         }
408 
409         // Any sensor not specified above must use a resolution of 1.
410         return 1.0f;
411     }
412 
hasMinResolutionRequirement(Sensor sensor)413     public static boolean hasMinResolutionRequirement(Sensor sensor) {
414         switch (sensor.getType()) {
415             case Sensor.TYPE_ACCELEROMETER:
416             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
417             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
418             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
419             case Sensor.TYPE_GYROSCOPE:
420             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
421             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
422             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
423             case Sensor.TYPE_MAGNETIC_FIELD:
424             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
425             case Sensor.TYPE_SIGNIFICANT_MOTION:
426             case Sensor.TYPE_STEP_DETECTOR:
427             case Sensor.TYPE_STEP_COUNTER:
428             case Sensor.TYPE_HEART_RATE:
429             case Sensor.TYPE_STATIONARY_DETECT:
430             case Sensor.TYPE_MOTION_DETECT:
431             case Sensor.TYPE_LOW_LATENCY_OFFBODY_DETECT:
432                 return true;
433         }
434         return false;
435     }
436 
getRequiredMinResolutionForSensor(Sensor sensor)437     public static float getRequiredMinResolutionForSensor(Sensor sensor) {
438         switch (sensor.getType()) {
439             case Sensor.TYPE_ACCELEROMETER:
440             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
441             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
442             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
443             case Sensor.TYPE_GYROSCOPE:
444             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
445             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
446             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
447             case Sensor.TYPE_MAGNETIC_FIELD:
448             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
449                 // Accelerometer, gyroscope, and mag are expected to have at most 24 bits of
450                 // resolution. The minimum resolution calculation uses slightly more than twice
451                 // the maximum range because:
452                 //   1) the sensor must be able to report values from [-maxRange, maxRange] without
453                 //      saturating
454                 //   2) to allow for slight rounding errors
455                 return (float)(2.001f * sensor.getMaximumRange() / Math.pow(2, 24));
456         }
457 
458         // Any sensor not specified above must use a resolution of 1.
459         return 1.0f;
460     }
461 
sensorTypeShortString(int type)462     public static String sensorTypeShortString(int type) {
463         switch (type) {
464             case Sensor.TYPE_ACCELEROMETER:
465                 return "Accel";
466             case Sensor.TYPE_GYROSCOPE:
467                 return "Gyro";
468             case Sensor.TYPE_MAGNETIC_FIELD:
469                 return "Mag";
470             case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED:
471                 return "UncalAccel";
472             case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
473                 return "UncalGyro";
474             case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
475                 return "UncalMag";
476             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES:
477                 return "LimitedAccel";
478             case Sensor.TYPE_ACCELEROMETER_LIMITED_AXES_UNCALIBRATED:
479                 return "UncalLimitedAccel";
480             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES:
481                 return "LimitedGyro";
482             case Sensor.TYPE_GYROSCOPE_LIMITED_AXES_UNCALIBRATED:
483                 return "UncalLimitedGyro";
484             default:
485                 return "Type_" + type;
486         }
487     }
488 
489     public static class TestResultCollector {
490         private List<AssertionError> mErrorList = new ArrayList<>();
491         private List<String> mErrorStringList = new ArrayList<>();
492         private String mTestName;
493         private String mTag;
494 
TestResultCollector()495         public TestResultCollector() {
496             this("Test");
497         }
498 
TestResultCollector(String test)499         public TestResultCollector(String test) {
500             this(test, "SensorCtsTest");
501         }
502 
TestResultCollector(String test, String tag)503         public TestResultCollector(String test, String tag) {
504             mTestName = test;
505             mTag = tag;
506         }
507 
perform(Runnable r)508         public void perform(Runnable r) {
509             perform(r, "");
510         }
511 
perform(Runnable r, String s)512         public void perform(Runnable r, String s) {
513             try {
514                 Log.d(mTag, mTestName + " running " + (s.isEmpty() ? "..." : s));
515                 r.run();
516             } catch (AssertionError e) {
517                 mErrorList.add(e);
518                 mErrorStringList.add(s);
519                 Log.e(mTag, mTestName + " error: " + e.getMessage());
520             }
521         }
522 
judge()523         public void judge() throws AssertionError {
524             if (mErrorList.isEmpty() && mErrorStringList.isEmpty()) {
525                 return;
526             }
527 
528             if (mErrorList.size() != mErrorStringList.size()) {
529                 throw new IllegalStateException("Mismatch error and error message");
530             }
531 
532             StringBuffer buf = new StringBuffer();
533             for (int i = 0; i < mErrorList.size(); ++i) {
534                 buf.append("Test (").append(mErrorStringList.get(i)).append(") - Error: ")
535                     .append(mErrorList.get(i).getMessage()).append("; ");
536             }
537             throw new AssertionError(buf.toString());
538         }
539     }
540 
bytesToHex(byte[] bytes, int offset, int length)541     public static String bytesToHex(byte[] bytes, int offset, int length) {
542         if (offset == -1) {
543             offset = 0;
544         }
545 
546         if (length == -1) {
547             length = bytes.length;
548         }
549 
550         final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
551         char[] hexChars = new char[length * 3];
552         int v;
553         for (int i = 0; i < length; i++) {
554             v = bytes[offset + i] & 0xFF;
555             hexChars[i * 3] = hexArray[v >>> 4];
556             hexChars[i * 3 + 1] = hexArray[v & 0x0F];
557             hexChars[i * 3 + 2] = ' ';
558         }
559         return new String(hexChars);
560     }
561 
makeMyPackageActive()562     public static void makeMyPackageActive() throws IOException {
563         final String command = "cmd sensorservice reset-uid-state "
564                 +  InstrumentationRegistry.getTargetContext().getPackageName()
565                 + " --user " + Process.myUserHandle().getIdentifier();
566         SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
567     }
568 
makeMyPackageIdle()569     public static void makeMyPackageIdle() throws IOException {
570         final String command = "cmd sensorservice set-uid-state "
571                 + InstrumentationRegistry.getTargetContext().getPackageName() + " idle"
572                 + " --user " + Process.myUserHandle().getIdentifier();
573         SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
574     }
575 }
576