1 /* 2 * Copyright (C) 2022 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 17 package android.hardware.cts; 18 19 import static org.junit.Assert.*; 20 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.hardware.Sensor; 24 import android.hardware.SensorEvent; 25 import android.hardware.SensorEventCallback; 26 import android.hardware.SensorManager; 27 import android.hardware.cts.accessories.VirtualHeadTracker; 28 import android.hardware.cts.helpers.SensorTestStateNotSupportedException; 29 import android.hardware.sensor.cts.R; 30 import android.os.Build; 31 import android.platform.test.annotations.AppModeFull; 32 import android.util.Log; 33 34 import androidx.test.InstrumentationRegistry; 35 36 import com.android.compatibility.common.util.CddTest; 37 import com.android.compatibility.common.util.PropertyUtil; 38 import com.android.compatibility.common.util.SystemUtil; 39 40 import java.io.IOException; 41 import java.util.Objects; 42 import java.util.concurrent.CountDownLatch; 43 import java.util.concurrent.TimeUnit; 44 45 @AppModeFull(reason = "Test requires special permission not available to instant apps") 46 public class SensorHeadTrackerTest extends SensorTestCase { 47 private static final String TAG = "SensorHeadTrackerTest"; 48 49 private SensorManager mSensorManager; 50 private boolean mHasSensorDynamicHeadTracker = false; 51 private Callback mCallback; 52 private Integer mSensorId; 53 private VirtualHeadTracker mVirtualHeadTracker; 54 private static final String VIRTUAL_HEAD_TRACKER_PRIMARY = "Emulated HT"; 55 private static final String VIRTUAL_HEAD_TRACKER_SECONDARY = "Emulated HT Secondary"; 56 private static final int CONNECTION_TIMEOUT_SEC = 5; 57 private static final int DISCONNECTION_TIMEOUT_SEC = 5; 58 private static final int FLUSH_COMPLETED_TIMEOUT_SEC = 5; 59 private static final int EVENT_TIMEOUT_SEC = 5; 60 61 @Override setUp()62 protected void setUp() throws InterruptedException { 63 if (PropertyUtil.getVsrApiLevel() < Build.VERSION_CODES.TIRAMISU) { 64 throw new SensorTestStateNotSupportedException( 65 "Test only applies for HAL version T or later, skip."); 66 } 67 configureHtSensorAccess(true); 68 mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE); 69 mHasSensorDynamicHeadTracker = 70 getContext() 71 .getPackageManager() 72 .hasSystemFeature(PackageManager.FEATURE_SENSOR_DYNAMIC_HEAD_TRACKER); 73 Log.d(TAG, String.format("mHasSensorDynamicHeadTracker %b", mHasSensorDynamicHeadTracker)); 74 75 mCallback = new Callback(); 76 mVirtualHeadTracker = new VirtualHeadTracker(); 77 mSensorManager.registerDynamicSensorCallback(mCallback, mVirtualHeadTracker.handler); 78 mVirtualHeadTracker.registerDevice(R.raw.head_tracker_main); 79 80 try { 81 // Note that we didn't skip the test here completely since 82 // testIsNotDynamicSensorDiscoverySupported need to be ran without 83 // dynamic sensors support 84 featureSupportedOrSkip(); 85 } catch (SensorTestStateNotSupportedException e) { 86 return; 87 } 88 89 assertTrue( 90 "Cannot detect sensor connection.", 91 mCallback.waitForConnection(VIRTUAL_HEAD_TRACKER_PRIMARY)); 92 } 93 94 @Override tearDown()95 protected void tearDown() { 96 Log.d(TAG, "Teardown."); 97 mVirtualHeadTracker.closeDevice(); 98 mCallback.waitForDisconnection(); 99 configureHtSensorAccess(false); 100 mSensorManager.unregisterDynamicSensorCallback(mCallback); 101 } 102 103 @CddTest(requirements = {"7.3"}) testIsNotDynamicSensorDiscoverySupported()104 public void testIsNotDynamicSensorDiscoverySupported() { 105 if (!mHasSensorDynamicHeadTracker && mSensorManager.isDynamicSensorDiscoverySupported()) { 106 assertFalse( 107 "Discovered head tracker sensor but feature flag is not set.", 108 mCallback.waitForConnection(VIRTUAL_HEAD_TRACKER_PRIMARY)); 109 } 110 } 111 112 @CddTest(requirements = {"7.3"}) testIsDynamicSensorDiscoverySupported()113 public void testIsDynamicSensorDiscoverySupported() { 114 featureSupportedOrSkip(); 115 assertTrue( 116 "Dynamic sensor discovery is not supported.", 117 mSensorManager.isDynamicSensorDiscoverySupported()); 118 } 119 120 @CddTest(requirements = {"7.3"}) testRegisterDynamicSensorCallback()121 public void testRegisterDynamicSensorCallback() { 122 featureSupportedOrSkip(); 123 assertTrue( 124 String.format( 125 "Sensor type is %d. Sensor type should be %d", 126 mCallback.mSensor.getType(), Sensor.TYPE_HEAD_TRACKER), 127 mCallback.isTypeHeadTracker()); 128 129 assertTrue("Sensor is a wake up sensor.", !mCallback.isNotWakeUpSensor()); 130 131 assertTrue("Sensor is not a dynamic sensor.", mCallback.isDynamicSensor()); 132 133 assertTrue( 134 String.format("Sensor id is %d. Must not be 0 or 1.", mCallback.mSensor.getId()), 135 mCallback.checkId()); 136 137 assertTrue( 138 String.format( 139 "Reporting mode is %d. Reporting mode should be %d.", 140 mCallback.mSensor.getReportingMode(), Sensor.REPORTING_MODE_CONTINUOUS), 141 mCallback.isReportingModeContinuous()); 142 143 assertTrue( 144 String.format( 145 "Sensor string type is %s. Sensor string should be %s.", 146 mCallback.mSensor.getStringType(), Sensor.STRING_TYPE_HEAD_TRACKER), 147 mCallback.isStringTypeHeadTracker()); 148 149 assertTrue("Sensor name is incorrect.", mCallback.isUhidName()); 150 151 assertTrue("Sensor is not in dynamic sensor list.", mCallback.isSensorInList()); 152 } 153 154 @CddTest(requirements = {"7.3"}) testArrayOfSixFloats()155 public void testArrayOfSixFloats() { 156 featureSupportedOrSkip(); 157 mCallback.headTrackerData(); 158 } 159 160 @CddTest(requirements = {"7.3"}) testDiscontinuity()161 public void testDiscontinuity() { 162 featureSupportedOrSkip(); 163 mVirtualHeadTracker.incDiscontinuityCount(); 164 165 assertTrue( 166 "First event after discontinuity was not called.", mCallback.discontinuityCount()); 167 } 168 169 @CddTest(requirements = {"7.3"}) testSensorManagerFlush()170 public void testSensorManagerFlush() { 171 featureSupportedOrSkip(); 172 assertTrue("Flush was not completed within five seconds.", mCallback.waitForFlush()); 173 } 174 175 @CddTest(requirements = {"7.3"}) testDisconnectionReconnection()176 public void testDisconnectionReconnection() { 177 featureSupportedOrSkip(); 178 mSensorId = mCallback.getSensorId(); 179 180 mVirtualHeadTracker.closeDevice(); 181 182 assertTrue("Device was not disconnected.", mCallback.waitForDisconnection()); 183 184 mVirtualHeadTracker.registerDevice(R.raw.head_tracker_main); 185 186 assertTrue("Cannot detect sensor reconnection.", 187 mCallback.waitForConnection(VIRTUAL_HEAD_TRACKER_PRIMARY)); 188 189 Integer sensorId = mCallback.getSensorId(); 190 boolean match = 191 mSensorId != null 192 && sensorId != null 193 && sensorId.intValue() == mSensorId.intValue(); 194 195 assertTrue("ID mismatch for the reconnected sensor.", match); 196 } 197 198 @CddTest(requirements = {"7.3"}) testAddSecondSensor()199 public void testAddSecondSensor() { 200 featureSupportedOrSkip(); 201 Callback secondCallback = new Callback(); 202 VirtualHeadTracker virtualHeadTrackerTwo = new VirtualHeadTracker(); 203 mSensorManager.registerDynamicSensorCallback(secondCallback, virtualHeadTrackerTwo.handler); 204 virtualHeadTrackerTwo.registerDevice(R.raw.head_tracker_distinct_id); 205 try { 206 assertTrue( 207 "Cannot detect second sensor connection.", 208 secondCallback.waitForConnection(VIRTUAL_HEAD_TRACKER_SECONDARY)); 209 assertTrue( 210 String.format( 211 "Sensor id is %d. Must not be 0 or 1.", mCallback.mSensor.getId()), 212 mCallback.checkId()); 213 214 assertTrue( 215 String.format( 216 "Sensor id is %d. Must not be 0 or 1.", secondCallback.mSensor.getId()), 217 secondCallback.checkId()); 218 219 assertNotEquals( 220 "Sensors have the same id.", 221 secondCallback.getSensorId(), 222 mCallback.getSensorId()); 223 } finally { 224 if (Objects.nonNull(virtualHeadTrackerTwo)) { 225 virtualHeadTrackerTwo.closeDevice(); 226 assertTrue( 227 "Did not get disconnect callback from the second sensor.", 228 secondCallback.waitForDisconnection()); 229 } 230 mSensorManager.unregisterDynamicSensorCallback(secondCallback); 231 } 232 } 233 234 private class Callback extends SensorManager.DynamicSensorCallback { 235 236 Sensor mSensor = null; 237 private boolean mFirstDiscontinuityReported; 238 private int mDuplicateDiscontinuityCount = 0; 239 private String mExpectedSensorName; 240 private CountDownLatch mConnectLatch; 241 private CountDownLatch mDisconnectLatch; 242 private final String mEmulatorName = "Emulated HT"; 243 244 @Override onDynamicSensorConnected(Sensor sensor)245 public void onDynamicSensorConnected(Sensor sensor) { 246 Log.d(TAG, "Sensor Connected: " + sensor); 247 if (mExpectedSensorName == null || mExpectedSensorName == sensor.getName()) { 248 mSensor = sensor; 249 if (mConnectLatch != null) { 250 mConnectLatch.countDown(); 251 } 252 } 253 } 254 255 @Override onDynamicSensorDisconnected(Sensor sensor)256 public void onDynamicSensorDisconnected(Sensor sensor) { 257 Log.d(TAG, "Sensor disconnected: " + sensor); 258 if (mSensor == sensor) { 259 mSensor = null; 260 if (mDisconnectLatch != null) { 261 mDisconnectLatch.countDown(); 262 } 263 } 264 } 265 waitForConnection(String sensorName)266 public boolean waitForConnection(String sensorName) { 267 boolean ret; 268 mExpectedSensorName = sensorName; 269 mConnectLatch = new CountDownLatch(1); 270 try { 271 ret = mConnectLatch.await(CONNECTION_TIMEOUT_SEC, TimeUnit.SECONDS); 272 } catch (InterruptedException e) { 273 ret = false; 274 Thread.currentThread().interrupt(); 275 } finally { 276 mConnectLatch = null; 277 } 278 return ret; 279 } 280 waitForDisconnection()281 public boolean waitForDisconnection() { 282 boolean ret; 283 mDisconnectLatch = new CountDownLatch(1); 284 try { 285 ret = mDisconnectLatch.await(DISCONNECTION_TIMEOUT_SEC, TimeUnit.SECONDS); 286 } catch (InterruptedException e) { 287 ret = false; 288 Thread.currentThread().interrupt(); 289 } finally { 290 mDisconnectLatch = null; 291 } 292 return ret; 293 } 294 waitForFlush()295 public boolean waitForFlush() { 296 final CountDownLatch eventLatch = new CountDownLatch(1); 297 298 SensorEventCallback eventCallback = 299 new SensorEventCallback() { 300 @Override 301 public void onFlushCompleted(Sensor s) { 302 eventLatch.countDown(); 303 } 304 }; 305 306 assertTrue( 307 "Register listener is false in waitForFlush().", 308 mSensorManager.registerListener( 309 eventCallback, mSensor, SensorManager.SENSOR_DELAY_NORMAL)); 310 311 mSensorManager.flush(eventCallback); 312 313 boolean flush; 314 try { 315 flush = eventLatch.await(FLUSH_COMPLETED_TIMEOUT_SEC, TimeUnit.SECONDS); 316 } catch (InterruptedException e) { 317 flush = false; 318 Thread.currentThread().interrupt(); 319 } finally { 320 mSensorManager.unregisterListener(eventCallback); 321 } 322 return flush; 323 } 324 325 private class SensorEventCopy { 326 private float[] mValues; 327 } 328 getOneSensorEvent()329 SensorEventCopy getOneSensorEvent() { 330 SensorEventCopy eventCopy = new SensorEventCopy(); 331 332 final CountDownLatch eventLatch = new CountDownLatch(1); 333 SensorEventCallback eventCallback = 334 new SensorEventCallback() { 335 public void onSensorChanged(SensorEvent event) { 336 if (eventLatch.getCount() == 1) { 337 eventCopy.mValues = event.values.clone(); 338 eventLatch.countDown(); 339 } 340 } 341 }; 342 343 assertTrue( 344 "Register listener is false in getOneSensorEvent().", 345 mSensorManager.registerListener( 346 eventCallback, mSensor, SensorManager.SENSOR_DELAY_NORMAL)); 347 348 try { 349 eventLatch.await(EVENT_TIMEOUT_SEC, TimeUnit.SECONDS); 350 } catch (InterruptedException e) { 351 Thread.currentThread().interrupt(); 352 } finally { 353 mSensorManager.unregisterListener(eventCallback); 354 } 355 356 return eventCopy; 357 } 358 discontinuityCount()359 public boolean discontinuityCount() { 360 final CountDownLatch eventLatch = new CountDownLatch(4); 361 SensorEventCallback eventCallback = 362 new SensorEventCallback() { 363 public void onSensorChanged(SensorEvent event) { 364 if (eventLatch.getCount() == 4) { 365 if (event.firstEventAfterDiscontinuity) { 366 mFirstDiscontinuityReported = true; 367 eventLatch.countDown(); 368 } // else: ignore event 369 } else { 370 if (event.firstEventAfterDiscontinuity) { 371 mDuplicateDiscontinuityCount++; 372 } 373 eventLatch.countDown(); 374 } 375 } 376 }; 377 378 assertTrue( 379 "Register listener is returning false in discontinuityCount().", 380 mSensorManager.registerListener( 381 eventCallback, mSensor, SensorManager.SENSOR_DELAY_NORMAL)); 382 383 try { 384 eventLatch.await(EVENT_TIMEOUT_SEC, TimeUnit.SECONDS); 385 assertEquals( 386 "mDuplicateDiscontinuityCount is not equal to expected.", 387 0, 388 mDuplicateDiscontinuityCount); 389 } catch (InterruptedException e) { 390 Thread.currentThread().interrupt(); 391 return false; 392 } finally { 393 mSensorManager.unregisterListener(eventCallback); 394 } 395 396 return mFirstDiscontinuityReported; 397 } 398 headTrackerData()399 public void headTrackerData() { 400 float threshold = 0.001f; 401 402 SensorEventCopy event = mCallback.getOneSensorEvent(); 403 404 assertNotNull("Time out waiting on a SensorEvent", event); 405 406 for (int i = 0; i < mVirtualHeadTracker.HEAD_TRACKER_VALUES.length; i++) { 407 assertEquals( 408 mVirtualHeadTracker.HEAD_TRACKER_VALUES[i], event.mValues[i], threshold); 409 } 410 } 411 isTypeHeadTracker()412 public boolean isTypeHeadTracker() { 413 return assumeSensorIsSet() && mSensor.getType() == Sensor.TYPE_HEAD_TRACKER; 414 } 415 isStringTypeHeadTracker()416 public boolean isStringTypeHeadTracker() { 417 return assumeSensorIsSet() 418 && mSensor.getStringType() == Sensor.STRING_TYPE_HEAD_TRACKER; 419 } 420 isNotWakeUpSensor()421 public boolean isNotWakeUpSensor() { 422 return assumeSensorIsSet() && mSensor.isWakeUpSensor(); 423 } 424 isDynamicSensor()425 public boolean isDynamicSensor() { 426 return assumeSensorIsSet() && mSensor.isDynamicSensor(); 427 } 428 checkId()429 public boolean checkId() { 430 return assumeSensorIsSet() && mSensor.getId() != 0 && mSensor.getId() != -1; 431 } 432 isReportingModeContinuous()433 public boolean isReportingModeContinuous() { 434 return assumeSensorIsSet() 435 && mSensor.getReportingMode() == Sensor.REPORTING_MODE_CONTINUOUS; 436 } 437 isUhidName()438 public boolean isUhidName() { 439 return assumeSensorIsSet() && mSensor.getName() == mEmulatorName; 440 } 441 isSensorInList()442 public boolean isSensorInList() { 443 return assumeSensorIsSet() 444 && mSensorManager.getDynamicSensorList(Sensor.TYPE_ALL).contains(mSensor); 445 } 446 isSensorInListOfSpecificType()447 public boolean isSensorInListOfSpecificType() { 448 return assumeSensorIsSet() 449 && mSensorManager.getDynamicSensorList(mSensor.getType()).contains(mSensor); 450 } 451 getSensorId()452 public Integer getSensorId() { 453 return assumeSensorIsSet() ? mSensor.getId() : null; 454 } 455 assumeSensorIsSet()456 private boolean assumeSensorIsSet() { 457 if (mSensor == null) { 458 Log.e(TAG, "Sensor is not set."); 459 return false; 460 } 461 return true; 462 } 463 } 464 featureSupportedOrSkip()465 private void featureSupportedOrSkip() { 466 if (!mHasSensorDynamicHeadTracker) { 467 throw new SensorTestStateNotSupportedException( 468 "Dynamic sensor discovery not supported, skip."); 469 } 470 } 471 configureHtSensorAccess(boolean enable)472 private static void configureHtSensorAccess(boolean enable) { 473 final String command = "cmd sensorservice " + ((enable) ? "un" : "") + "restrict-ht"; 474 Log.d(TAG, "Running command " + command); 475 try { 476 SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command); 477 } catch (IOException e) { 478 Log.e(TAG, "Failed to run command " + command, e); 479 } 480 } 481 } 482