1 /* 2 * Copyright (C) 2021 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.media.audio.cts; 18 19 import static android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS; 20 import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY; 21 22 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 23 24 import static org.junit.Assert.assertThrows; 25 26 import android.annotation.NonNull; 27 import android.content.pm.PackageManager; 28 import android.media.AudioAttributes; 29 import android.media.AudioDeviceAttributes; 30 import android.media.AudioDeviceInfo; 31 import android.media.AudioFormat; 32 import android.media.AudioManager; 33 import android.media.Spatializer; 34 import android.media.audiofx.AudioEffect; 35 import android.os.SystemProperties; 36 import android.platform.test.annotations.RequiresFlagsEnabled; 37 import android.util.Log; 38 39 import com.android.compatibility.common.util.CtsAndroidTestCase; 40 import com.android.compatibility.common.util.FrameworkSpecificTest; 41 42 import org.junit.Assert; 43 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.concurrent.Executors; 47 import java.util.concurrent.LinkedBlockingQueue; 48 import java.util.concurrent.TimeUnit; 49 50 @FrameworkSpecificTest 51 public class SpatializerTest extends CtsAndroidTestCase { 52 53 private AudioManager mAudioManager; 54 private static final String TAG = "SpatializerTest"; 55 private static final int LISTENER_WAIT_TIMEOUT_MS = 3000; 56 57 @Override setUp()58 protected void setUp() throws Exception { 59 super.setUp(); 60 mAudioManager = (AudioManager) getContext().getSystemService(AudioManager.class); 61 } 62 63 @Override tearDown()64 protected void tearDown() throws Exception { 65 getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); 66 } 67 testGetSpatializer()68 public void testGetSpatializer() throws Exception { 69 Spatializer spat = mAudioManager.getSpatializer(); 70 assertNotNull("Spatializer shouldn't be null", spat); 71 } 72 testUnsupported()73 public void testUnsupported() throws Exception { 74 Spatializer spat = mAudioManager.getSpatializer(); 75 if (spat.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 76 Log.i(TAG, "skipping testUnsupported, functionality supported"); 77 return; 78 } 79 assertFalse(spat.isEnabled()); 80 assertFalse(spat.isAvailable()); 81 } 82 83 /** 84 * Test that if the device reports the property ro.audio.spatializer_enabled as true 85 * and has an audio effect of type EFFECT_TYPE_SPATIALIZER, 86 * then the Spatializer's immersive audio level has some spatialization capability 87 * @throws Exception when SPATIALIZER_IMMERSIVE_LEVEL_NONE is the reported immersive level 88 * by the Spatializer instance 89 */ testEffectSpatializer()90 public void testEffectSpatializer() throws Exception { 91 final boolean spatEnabled = SystemProperties.getBoolean("ro.audio.spatializer_enabled", 92 false); 93 if (!spatEnabled) { 94 Log.i(TAG, "testEffectSpatializer: spatializer_enabled false, skipping"); 95 return; 96 } 97 98 AudioEffect.Descriptor[] descriptors = AudioEffect.queryEffects(); 99 boolean hasSpatializer = false; 100 for (AudioEffect.Descriptor desc : descriptors) { 101 if (desc.type.equals(AudioEffect.EFFECT_TYPE_SPATIALIZER)) { 102 Log.i(TAG, "found EFFECT_TYPE_SPATIALIZER name:" + desc.name 103 + " / implementor:" + desc.implementor); 104 hasSpatializer = true; 105 break; 106 } 107 } 108 if (!hasSpatializer) { 109 Log.i(TAG, "testEffectSpatializer: no EFFECT_TYPE_SPATIALIZER found, skipping"); 110 return; 111 } 112 Spatializer spat = mAudioManager.getSpatializer(); 113 assertTrue("EFFECT_TYPE_SPATIALIZER found but not available for Spatializer", 114 spat.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE); 115 } 116 testSupportedDevices()117 public void testSupportedDevices() throws Exception { 118 Spatializer spat = mAudioManager.getSpatializer(); 119 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 120 Log.i(TAG, "skipping testSupportedDevices, functionality unsupported"); 121 return; 122 } 123 124 final AudioDeviceAttributes device = new AudioDeviceAttributes( 125 AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, "bla"); 126 // try to add/remove compatible device without permission, expect failure 127 assertThrows("Able to call addCompatibleAudioDevice without permission", 128 SecurityException.class, 129 () -> spat.addCompatibleAudioDevice(device)); 130 assertThrows("Able to call removeCompatibleAudioDevice without permission", 131 SecurityException.class, 132 () -> spat.removeCompatibleAudioDevice(device)); 133 assertThrows("Able to call getCompatibleAudioDevice without permission", 134 SecurityException.class, 135 () -> spat.getCompatibleAudioDevices()); 136 assertThrows("Able to call isAvailableForDevice without permission", 137 SecurityException.class, 138 () -> spat.isAvailableForDevice(device)); 139 assertThrows("Able to call hasHeadTracker without permission", 140 SecurityException.class, 141 () -> spat.hasHeadTracker(device)); 142 assertThrows("Able to call setHeadTrackerEnabled without permission", 143 SecurityException.class, 144 () -> spat.setHeadTrackerEnabled(true, device)); 145 assertThrows("Able to call isHeadTrackerEnabled without permission", 146 SecurityException.class, 147 () -> spat.isHeadTrackerEnabled(device)); 148 149 // try again with permission, then add a device and remove it 150 getInstrumentation().getUiAutomation() 151 .adoptShellPermissionIdentity("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"); 152 spat.addCompatibleAudioDevice(device); 153 List<AudioDeviceAttributes> compatDevices = spat.getCompatibleAudioDevices(); 154 assertTrue("added device not in list of compatible devices", 155 compatDevices.contains(device)); 156 assertTrue("compatible device should be available", spat.isAvailableForDevice(device)); 157 if (spat.hasHeadTracker(device)) { 158 spat.setHeadTrackerEnabled(true, device); 159 assertTrue("head tracker not found enabled", spat.isHeadTrackerEnabled(device)); 160 spat.setHeadTrackerEnabled(false, device); 161 assertFalse("head tracker not found disabled", spat.isHeadTrackerEnabled(device)); 162 } 163 spat.removeCompatibleAudioDevice(device); 164 compatDevices = spat.getCompatibleAudioDevices(); 165 assertFalse("removed device still in list of compatible devices", 166 compatDevices.contains(device)); 167 168 getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); 169 } 170 171 @RequiresFlagsEnabled(FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY) testLowLatencyHeadtrackingFeature()172 public void testLowLatencyHeadtrackingFeature() throws Exception { 173 Spatializer spat = mAudioManager.getSpatializer(); 174 if (spat.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 175 return; 176 } 177 assertFalse("Cannot have SPATIALIZER_IMMERSIVE_LEVEL_NONE with feature " 178 + "FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY declared", 179 getContext().getPackageManager().hasSystemFeature( 180 PackageManager.FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY)); 181 } 182 testHeadTrackingListener()183 public void testHeadTrackingListener() throws Exception { 184 Spatializer spat = mAudioManager.getSpatializer(); 185 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 186 Log.i(TAG, "skipping testHeadTrackingListener, functionality unsupported"); 187 return; 188 } 189 190 // try to call any head tracking method without permission 191 assertThrows("Able to call getHeadTrackingMode without permission", 192 SecurityException.class, 193 () -> spat.getHeadTrackingMode()); 194 assertThrows("Able to call getDesiredHeadTrackingMode without permission", 195 SecurityException.class, 196 () -> spat.getDesiredHeadTrackingMode()); 197 assertThrows("Able to call getSupportedHeadTrackingModes without permission", 198 SecurityException.class, 199 () -> spat.getSupportedHeadTrackingModes()); 200 assertThrows("Able to call setDesiredHeadTrackingMode without permission", 201 SecurityException.class, 202 () -> spat.setDesiredHeadTrackingMode( 203 Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE)); 204 final MyHeadTrackingModeListener listener = new MyHeadTrackingModeListener(); 205 assertThrows("Able to call addOnHeadTrackingModeChangedListener without permission", 206 SecurityException.class, 207 () -> spat.addOnHeadTrackingModeChangedListener(Executors.newSingleThreadExecutor(), 208 listener)); 209 getInstrumentation().getUiAutomation() 210 .adoptShellPermissionIdentity("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"); 211 212 // argument validation 213 assertThrows("Able to call addOnHeadTrackingModeChangedListener with null Executor", 214 NullPointerException.class, 215 () -> spat.addOnHeadTrackingModeChangedListener(null, listener)); 216 assertThrows("Able to call addOnHeadTrackingModeChangedListener with null listener", 217 NullPointerException.class, 218 () -> spat.addOnHeadTrackingModeChangedListener(Executors.newSingleThreadExecutor(), 219 null)); 220 assertThrows("Able to call removeOnHeadTrackingModeChangedListener with null listener", 221 NullPointerException.class, 222 () -> spat.removeOnHeadTrackingModeChangedListener(null)); 223 224 // test of functionality 225 spat.setEnabled(true); 226 List<Integer> supportedModes = spat.getSupportedHeadTrackingModes(); 227 Assert.assertNotNull("Invalid null list of tracking modes", supportedModes); 228 Log.i(TAG, "Reported supported head tracking modes:" + supportedModes); 229 if (!(supportedModes.contains(Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE) 230 || supportedModes.contains(Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD) 231 || supportedModes.contains(Spatializer.HEAD_TRACKING_MODE_OTHER))) { 232 // no head tracking is supported, verify it is correctly reported by the API 233 Log.i(TAG, "no headtracking modes supported"); 234 assertEquals("When no head tracking mode supported, list of modes must be empty", 235 0, supportedModes.size()); 236 assertEquals("Invalid mode when no head tracking mode supported", 237 Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED, spat.getHeadTrackingMode()); 238 // verify you can't enable head tracking on a device 239 final AudioDeviceAttributes device = new AudioDeviceAttributes( 240 AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, "bli"); 241 spat.addCompatibleAudioDevice(device); 242 spat.setHeadTrackerEnabled(true, device); 243 assertFalse(spat.isHeadTrackerEnabled(device)); 244 return; 245 } 246 int trackingModeToUse; 247 if (supportedModes.contains(Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE)) { 248 trackingModeToUse = Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE; 249 } else { 250 trackingModeToUse = Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD; 251 } 252 spat.setDesiredHeadTrackingMode(Spatializer.HEAD_TRACKING_MODE_DISABLED); 253 spat.addOnHeadTrackingModeChangedListener(Executors.newSingleThreadExecutor(), listener); 254 spat.setDesiredHeadTrackingMode(trackingModeToUse); 255 Integer observedDesired = listener.getDesired(); 256 assertNotNull("No desired head tracking mode change reported", observedDesired); 257 assertEquals("Wrong reported desired tracking mode", trackingModeToUse, 258 observedDesired.intValue()); 259 assertEquals("Set desired mode not returned by getter", spat.getDesiredHeadTrackingMode(), 260 trackingModeToUse); 261 final int actualMode = spat.getHeadTrackingMode(); 262 // not failing test if modes differ, just logging 263 if (trackingModeToUse != actualMode) { 264 Log.i(TAG, "head tracking mode desired:" + trackingModeToUse + " actual mode:" 265 + actualMode); 266 } 267 spat.removeOnHeadTrackingModeChangedListener(listener); 268 } 269 testSpatializerOutput()270 public void testSpatializerOutput() throws Exception { 271 Spatializer spat = mAudioManager.getSpatializer(); 272 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 273 Log.i(TAG, "skipping testSpatializerOutput, functionality unsupported"); 274 return; 275 } 276 277 // try to call any output method without permission 278 assertThrows("Able to call getOutput without permission", 279 SecurityException.class, 280 () -> spat.getOutput()); 281 final MyOutputChangedListener listener = new MyOutputChangedListener(); 282 assertThrows("Able to call setOnSpatializerOutputChangedListener without permission", 283 SecurityException.class, 284 () -> spat.setOnSpatializerOutputChangedListener( 285 Executors.newSingleThreadExecutor(), listener)); 286 assertThrows("Able to call clearOnSpatializerOutputChangedListener with no listener", 287 SecurityException.class, 288 () -> spat.clearOnSpatializerOutputChangedListener()); 289 290 getInstrumentation().getUiAutomation() 291 .adoptShellPermissionIdentity("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"); 292 293 // argument validation 294 assertThrows("Able to call setOnSpatializerOutputChangedListener with null Executor", 295 NullPointerException.class, 296 () -> spat.setOnSpatializerOutputChangedListener(null, listener)); 297 assertThrows("Able to call setOnSpatializerOutputChangedListener with null listener", 298 NullPointerException.class, 299 () -> spat.setOnSpatializerOutputChangedListener( 300 Executors.newSingleThreadExecutor(), null)); 301 302 spat.getOutput(); 303 // output doesn't change upon playback, so at this point only exercising 304 // registering / clearing of output listener under permission 305 spat.clearOnSpatializerOutputChangedListener(); // this is to clear the client listener ref 306 spat.setOnSpatializerOutputChangedListener(Executors.newSingleThreadExecutor(), listener); 307 spat.clearOnSpatializerOutputChangedListener(); 308 assertThrows("Able to call clearOnSpatializerOutputChangedListener with no listener", 309 IllegalStateException.class, 310 () -> spat.clearOnSpatializerOutputChangedListener()); 311 } 312 testExercisePose()313 public void testExercisePose() throws Exception { 314 Spatializer spat = mAudioManager.getSpatializer(); 315 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 316 Log.i(TAG, "skipping testExercisePose, functionality unsupported"); 317 return; 318 } 319 320 // argument validation 321 assertThrows("Able to call setGlobalTransform without a 6-float array", 322 IllegalArgumentException.class, 323 () -> spat.setGlobalTransform(new float[5])); 324 assertThrows("Able to call setGlobalTransform without a null array", 325 NullPointerException.class, 326 () -> spat.setGlobalTransform(null)); 327 final MyPoseUpdatedListener listener = new MyPoseUpdatedListener(); 328 assertThrows("Able to call setOnHeadToSoundstagePoseUpdatedListener with null Executor", 329 NullPointerException.class, 330 () -> spat.setOnHeadToSoundstagePoseUpdatedListener(null, listener)); 331 assertThrows("Able to call setOnHeadToSoundstagePoseUpdatedListener with null listener", 332 NullPointerException.class, 333 () -> spat.setOnHeadToSoundstagePoseUpdatedListener( 334 Executors.newSingleThreadExecutor(), null)); 335 assertThrows("Able to call clearOnHeadToSoundstagePoseUpdatedListener with no listener", 336 IllegalStateException.class, 337 () -> spat.clearOnHeadToSoundstagePoseUpdatedListener()); 338 339 getInstrumentation().getUiAutomation() 340 .adoptShellPermissionIdentity("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"); 341 // TODO once headtracking is properly reported: check pose changes on recenter and transform 342 spat.setOnHeadToSoundstagePoseUpdatedListener( 343 Executors.newSingleThreadExecutor(), listener); 344 // oneway call from client to AudioService, can't check for exception earlier 345 spat.recenterHeadTracker(); 346 // oneway call from client to AudioService, can't check for exception earler 347 spat.setGlobalTransform(new float[6]); 348 spat.clearOnHeadToSoundstagePoseUpdatedListener(); 349 } 350 testEffectParameters()351 public void testEffectParameters() throws Exception { 352 Spatializer spat = mAudioManager.getSpatializer(); 353 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 354 Log.i(TAG, "skipping testEffectParameters, functionality unsupported"); 355 return; 356 } 357 358 // argument validation 359 assertThrows("Able to call setEffectParameter with null value", 360 NullPointerException.class, 361 () -> spat.setEffectParameter(0, null)); 362 assertThrows("Able to call getEffectParameter with null value", 363 NullPointerException.class, 364 () -> spat.getEffectParameter(0, null)); 365 366 // permission check 367 byte[] val = new byte[4]; 368 assertThrows("Able to call setEffectParameter without permission", 369 SecurityException.class, 370 () -> spat.setEffectParameter(0, val)); 371 assertThrows("Able to call getEffectParameter without permission", 372 SecurityException.class, 373 () -> spat.getEffectParameter(0, val)); 374 } 375 testSpatializerStateListenerManagement()376 public void testSpatializerStateListenerManagement() throws Exception { 377 final Spatializer spat = mAudioManager.getSpatializer(); 378 final MySpatStateListener stateListener = new MySpatStateListener(); 379 380 // add listener: 381 // verify null arg checks 382 assertThrows("null Executor allowed in addOnSpatializerStateChangedListener", 383 NullPointerException.class, 384 () -> spat.addOnSpatializerStateChangedListener(null, stateListener)); 385 assertThrows("null listener allowed in addOnSpatializerStateChangedListener", 386 NullPointerException.class, 387 () -> spat.addOnSpatializerStateChangedListener( 388 Executors.newSingleThreadExecutor(), null)); 389 390 spat.addOnSpatializerStateChangedListener(Executors.newSingleThreadExecutor(), 391 stateListener); 392 // verify double add 393 assertThrows("duplicate listener allowed in addOnSpatializerStateChangedListener", 394 IllegalArgumentException.class, 395 () -> spat.addOnSpatializerStateChangedListener(Executors.newSingleThreadExecutor(), 396 stateListener)); 397 398 // remove listener: 399 // verify null arg check 400 assertThrows("null listener allowed in removeOnSpatializerStateChangedListener", 401 NullPointerException.class, 402 () -> spat.removeOnSpatializerStateChangedListener(null)); 403 404 // verify unregistered listener 405 assertThrows("unregistered listener allowed in removeOnSpatializerStateChangedListener", 406 IllegalArgumentException.class, 407 () -> spat.removeOnSpatializerStateChangedListener(new MySpatStateListener())); 408 409 spat.removeOnSpatializerStateChangedListener(stateListener); 410 // verify double remove 411 assertThrows("double listener removal allowed in removeOnSpatializerStateChangedListener", 412 IllegalArgumentException.class, 413 () -> spat.removeOnSpatializerStateChangedListener(stateListener)); 414 } 415 testMinSpatializationCapabilities()416 public void testMinSpatializationCapabilities() throws Exception { 417 Spatializer spat = mAudioManager.getSpatializer(); 418 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 419 Log.i(TAG, "skipping testMinSpatializationCapabilities, no Spatializer"); 420 return; 421 } 422 if (!spat.isAvailable()) { 423 Log.i(TAG, "skipping testMinSpatializationCapabilities, Spatializer not available"); 424 return; 425 } 426 for (int sampleRate : new int[] { 44100, 4800 }) { 427 AudioFormat minFormat = new AudioFormat.Builder() 428 .setSampleRate(sampleRate) 429 .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) 430 .setEncoding(AudioFormat.ENCODING_PCM_16BIT) 431 .build(); 432 for (int usage : new int[] { AudioAttributes.USAGE_MEDIA, 433 AudioAttributes.USAGE_GAME}) { 434 AudioAttributes defAttr = new AudioAttributes.Builder() 435 .setUsage(usage) 436 .build(); 437 assertTrue("AudioAttributes usage:" + usage + " at " + sampleRate 438 + " should be virtualizeable", spat.canBeSpatialized(defAttr, minFormat)); 439 } 440 } 441 } 442 testSpatializerDisabling()443 public void testSpatializerDisabling() throws Exception { 444 Spatializer spat = mAudioManager.getSpatializer(); 445 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 446 Log.i(TAG, "skipping testSpatializerDisabling, no Spatializer"); 447 return; 448 } 449 if (!spat.isAvailable()) { 450 Log.i(TAG, "skipping testSpatializerDisabling, Spatializer not available"); 451 return; 452 } 453 if (!spat.isEnabled()) { 454 // this test can only test disabling the feature, and thus requires 455 // to start with an "enabled" state, as a "disabled" state can reflect 456 // a number of internal states that can't always be reset (e.g. an uninitialized 457 // effect or a disabled feature) 458 Log.i(TAG, "skipping testSpatializerDisabling, Spatializer not enabled"); 459 return; 460 } 461 final MySpatStateListener stateListener = new MySpatStateListener(); 462 463 spat.addOnSpatializerStateChangedListener(Executors.newSingleThreadExecutor(), 464 stateListener); 465 getInstrumentation().getUiAutomation() 466 .adoptShellPermissionIdentity("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"); 467 try { 468 spat.setEnabled(false); 469 assertEquals("Spatializer not reported as disabled", false, spat.isEnabled()); 470 Boolean enabled = stateListener.getEnabled(); 471 assertNotNull("Spatializer state listener wasn't called", enabled); 472 assertEquals("Spatializer state listener didn't get expected value", 473 false, enabled.booleanValue()); 474 } finally { 475 // restore state 476 spat.setEnabled(true); 477 getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); 478 spat.removeOnSpatializerStateChangedListener(stateListener); 479 assertEquals("Spatializer state cannot be restored", true, spat.isEnabled()); 480 } 481 } 482 testHeadTrackerAvailable()483 public void testHeadTrackerAvailable() throws Exception { 484 Spatializer spat = mAudioManager.getSpatializer(); 485 if (spat.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { 486 Log.i(TAG, "skipping testHeadTrackerAvailable, no Spatializer"); 487 return; 488 } 489 final MyHeadTrackerAvailable htAvailableListener = new MyHeadTrackerAvailable(); 490 491 assertThrows("null Executor allowed in addOnHeadTrackerAvailableListener", 492 NullPointerException.class, 493 () -> spat.addOnHeadTrackerAvailableListener(null, htAvailableListener)); 494 assertThrows("null listener allowed in addOnHeadTrackerAvailableListener", 495 NullPointerException.class, 496 () -> spat.addOnHeadTrackerAvailableListener(Executors.newSingleThreadExecutor(), 497 null)); 498 spat.addOnHeadTrackerAvailableListener( 499 Executors.newSingleThreadExecutor(), htAvailableListener); 500 501 final boolean enabled = spat.isEnabled(); 502 // verify that with spatializer disabled, the head tracker is not available 503 if (!enabled) { 504 // spatializer not enabled 505 assertFalse("head tracker available despite spatializer disabled", 506 spat.isHeadTrackerAvailable()); 507 } else { 508 final MySpatStateListener stateListener = new MySpatStateListener(); 509 spat.addOnSpatializerStateChangedListener(Executors.newSingleThreadExecutor(), 510 stateListener); 511 // now disable the effect and check head tracker availability 512 // now disable the effect and check head tracker availability 513 runWithShellPermissionIdentity( 514 () -> { 515 spat.setEnabled(false); 516 }, 517 MODIFY_DEFAULT_AUDIO_EFFECTS); 518 assertFalse("spatializer state listener not notified after disabling", 519 stateListener.getEnabled()); 520 assertFalse("head tracker available despite spatializer disabled", 521 spat.isHeadTrackerAvailable()); 522 runWithShellPermissionIdentity( 523 () -> { 524 spat.setEnabled(true); 525 }, 526 MODIFY_DEFAULT_AUDIO_EFFECTS); 527 assertTrue("spatializer state listener not notified after enabling", 528 stateListener.getEnabled()); 529 } 530 assertThrows("null listener allowed in removeOnHeadTrackerAvailableListener", 531 NullPointerException.class, 532 () -> spat.removeOnHeadTrackerAvailableListener(null)); 533 spat.removeOnHeadTrackerAvailableListener(htAvailableListener); 534 assertThrows("able to remove listener twice in removeOnHeadTrackerAvailableListener", 535 IllegalArgumentException.class, 536 () -> spat.removeOnHeadTrackerAvailableListener(htAvailableListener)); 537 } 538 539 static class MySpatStateListener 540 implements Spatializer.OnSpatializerStateChangedListener { 541 542 private final LinkedBlockingQueue<Boolean> mEnabledQueue = 543 new LinkedBlockingQueue<Boolean>(1); 544 reset()545 void reset() { 546 mEnabledQueue.clear(); 547 } 548 getEnabled()549 Boolean getEnabled() throws Exception { 550 return mEnabledQueue.poll(LISTENER_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 551 } 552 MySpatStateListener()553 MySpatStateListener() { 554 reset(); 555 } 556 557 @Override onSpatializerEnabledChanged(Spatializer spat, boolean enabled)558 public void onSpatializerEnabledChanged(Spatializer spat, boolean enabled) { 559 Log.i(TAG, "onSpatializerEnabledChanged:" + enabled); 560 mEnabledQueue.offer(enabled); 561 } 562 563 @Override onSpatializerAvailableChanged(@onNull Spatializer spat, boolean available)564 public void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available) { 565 Log.i(TAG, "onSpatializerAvailableChanged:" + available); 566 } 567 } 568 569 static class MyHeadTrackingModeListener 570 implements Spatializer.OnHeadTrackingModeChangedListener { 571 private final LinkedBlockingQueue<Integer> mDesiredQueue = 572 new LinkedBlockingQueue<Integer>(1); 573 private final LinkedBlockingQueue<Integer> mRealQueue = 574 new LinkedBlockingQueue<Integer>(1); 575 576 @Override onHeadTrackingModeChanged(Spatializer spatializer, int mode)577 public void onHeadTrackingModeChanged(Spatializer spatializer, int mode) { 578 Log.i(TAG, "onHeadTrackingModeChanged:" + mode); 579 mRealQueue.offer(mode); 580 } 581 582 @Override onDesiredHeadTrackingModeChanged(Spatializer spatializer, int mode)583 public void onDesiredHeadTrackingModeChanged(Spatializer spatializer, int mode) { 584 Log.i(TAG, "onDesiredHeadTrackingModeChanged:" + mode); 585 mDesiredQueue.offer(mode); 586 } 587 getDesired()588 public Integer getDesired() throws Exception { 589 return mDesiredQueue.poll(LISTENER_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 590 } 591 } 592 593 static class MyOutputChangedListener 594 implements Spatializer.OnSpatializerOutputChangedListener { 595 @Override onSpatializerOutputChanged(Spatializer spatializer, int output)596 public void onSpatializerOutputChanged(Spatializer spatializer, int output) { 597 Log.i(TAG, "onSpatializerOutputChanged:" + output); 598 } 599 } 600 601 static class MyPoseUpdatedListener 602 implements Spatializer.OnHeadToSoundstagePoseUpdatedListener { 603 @Override onHeadToSoundstagePoseUpdated(Spatializer spatializer, float[] pose)604 public void onHeadToSoundstagePoseUpdated(Spatializer spatializer, float[] pose) { 605 Log.i(TAG, "onHeadToSoundstagePoseUpdated:" + Arrays.toString(pose)); 606 } 607 } 608 609 static class MyHeadTrackerAvailable implements Spatializer.OnHeadTrackerAvailableListener { 610 @Override onHeadTrackerAvailableChanged(Spatializer spatializer, boolean available)611 public void onHeadTrackerAvailableChanged(Spatializer spatializer, boolean available) { 612 Log.i(TAG, "onHeadTrackerAvailable(" + available + ")"); 613 } 614 } 615 } 616