• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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