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