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