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