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;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.RequiresPermission;
25 import android.annotation.SystemApi;
26 import android.media.permission.ClearCallingIdentityContext;
27 import android.media.permission.SafeCloseable;
28 import android.os.RemoteException;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.GuardedBy;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Objects;
38 import java.util.concurrent.Executor;
39 
40 /**
41  * Spatializer provides access to querying capabilities and behavior of sound spatialization
42  * on the device.
43  * Sound spatialization simulates sounds originating around the listener as if they were coming
44  * from virtual speakers placed around the listener.<br>
45  * Support for spatialization is optional, use {@link AudioManager#getSpatializer()} to obtain an
46  * instance of this class if the feature is supported.
47  *
48  */
49 public class Spatializer {
50 
51     private final @NonNull AudioManager mAm;
52 
53     private static final String TAG = "Spatializer";
54 
55     /**
56      * @hide
57      * Constructor with AudioManager acting as proxy to AudioService
58      * @param am a non-null AudioManager
59      */
Spatializer(@onNull AudioManager am)60     protected Spatializer(@NonNull AudioManager am) {
61         mAm = Objects.requireNonNull(am);
62     }
63 
64     /**
65      * Returns whether spatialization is enabled or not.
66      * A false value can originate for instance from the user electing to
67      * disable the feature, or when the feature is not supported on the device (indicated
68      * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}).
69      * <br>
70      * Note that this state reflects a platform-wide state of the "desire" to use spatialization,
71      * but availability of the audio processing is still dictated by the compatibility between
72      * the effect and the hardware configuration, as indicated by {@link #isAvailable()}.
73      * @return {@code true} if spatialization is enabled
74      * @see #isAvailable()
75      */
isEnabled()76     public boolean isEnabled() {
77         try {
78             return mAm.getService().isSpatializerEnabled();
79         } catch (RemoteException e) {
80             Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e);
81             return false;
82         }
83     }
84 
85     /**
86      * Returns whether spatialization is available.
87      * Reasons for spatialization being unavailable include situations where audio output is
88      * incompatible with sound spatialization, such as playback on a monophonic speaker.<br>
89      * Note that spatialization can be available, but disabled by the user, in which case this
90      * method would still return {@code true}, whereas {@link #isEnabled()}
91      * would return {@code false}.<br>
92      * Also when the feature is not supported on the device (indicated
93      * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}),
94      * the return value will be false.
95      * @return {@code true} if the spatializer effect is available and capable
96      *         of processing the audio for the current configuration of the device,
97      *         {@code false} otherwise.
98      * @see #isEnabled()
99      */
isAvailable()100     public boolean isAvailable()  {
101         try {
102             return mAm.getService().isSpatializerAvailable();
103         } catch (RemoteException e) {
104             Log.e(TAG, "Error querying isSpatializerAvailable, returning false", e);
105             return false;
106         }
107     }
108 
109     /** @hide */
110     @IntDef(flag = false, value = {
111             SPATIALIZER_IMMERSIVE_LEVEL_OTHER,
112             SPATIALIZER_IMMERSIVE_LEVEL_NONE,
113             SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
114     })
115     @Retention(RetentionPolicy.SOURCE)
116     public @interface ImmersiveAudioLevel {};
117 
118     /**
119      * Constant indicating the {@code Spatializer} on this device supports a spatialization
120      * mode that differs from the ones available at this SDK level.
121      * @see #getImmersiveAudioLevel()
122      */
123     public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
124 
125     /**
126      * Constant indicating there are no spatialization capabilities supported on this device.
127      * @see #getImmersiveAudioLevel()
128      */
129     public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
130 
131     /**
132      * Constant indicating the {@code Spatializer} on this device supports multichannel
133      * spatialization.
134      * @see #getImmersiveAudioLevel()
135      */
136     public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
137 
138     /**
139      * @hide
140      * Constant indicating the {@code Spatializer} on this device supports the spatialization of
141      * multichannel bed plus objects.
142      * @see #getImmersiveAudioLevel()
143      */
144     public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2;
145 
146     /** @hide */
147     @IntDef(flag = false, value = {
148             HEAD_TRACKING_MODE_UNSUPPORTED,
149             HEAD_TRACKING_MODE_DISABLED,
150             HEAD_TRACKING_MODE_RELATIVE_WORLD,
151             HEAD_TRACKING_MODE_RELATIVE_DEVICE,
152     }) public @interface HeadTrackingMode {};
153 
154     /** @hide */
155     @IntDef(flag = false, value = {
156             HEAD_TRACKING_MODE_DISABLED,
157             HEAD_TRACKING_MODE_RELATIVE_WORLD,
158             HEAD_TRACKING_MODE_RELATIVE_DEVICE,
159     }) public @interface HeadTrackingModeSet {};
160 
161     /** @hide */
162     @IntDef(flag = false, value = {
163             HEAD_TRACKING_MODE_RELATIVE_WORLD,
164             HEAD_TRACKING_MODE_RELATIVE_DEVICE,
165     }) public @interface HeadTrackingModeSupported {};
166 
167     /**
168      * @hide
169      * Constant indicating head tracking is not supported by this {@code Spatializer}
170      * @see #getHeadTrackingMode()
171      */
172     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
173     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
174     public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2;
175 
176     /**
177      * @hide
178      * Constant indicating head tracking is disabled on this {@code Spatializer}
179      * @see #getHeadTrackingMode()
180      */
181     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
182     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
183     public static final int HEAD_TRACKING_MODE_DISABLED = -1;
184 
185     /**
186      * @hide
187      * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an
188      * error state but represents a customized behavior not defined by this API.
189      * @see #getHeadTrackingMode()
190      */
191     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
192     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
193     public static final int HEAD_TRACKING_MODE_OTHER = 0;
194 
195     /**
196      * @hide
197      * Constant indicating head tracking is tracking the user's position / orientation relative to
198      * the world around them
199      * @see #getHeadTrackingMode()
200      */
201     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
202     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
203     public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1;
204 
205     /**
206      * @hide
207      * Constant indicating head tracking is tracking the user's position / orientation relative to
208      * the device
209      * @see #getHeadTrackingMode()
210      */
211     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
212     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
213     public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2;
214 
215     /**
216      * Return the level of support for the spatialization feature on this device.
217      * This level of support is independent of whether the {@code Spatializer} is currently
218      * enabled or available and will not change over time.
219      * @return the level of spatialization support
220      * @see #isEnabled()
221      * @see #isAvailable()
222      */
getImmersiveAudioLevel()223     public @ImmersiveAudioLevel int getImmersiveAudioLevel() {
224         int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
225         try {
226             level = mAm.getService().getSpatializerImmersiveAudioLevel();
227         } catch (Exception e) { /* using NONE */ }
228         return level;
229     }
230 
231     /**
232      * @hide
233      * Enables / disables the spatializer effect.
234      * Changing the enabled state will trigger the public
235      * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)}
236      * registered listeners.
237      * @param enabled {@code true} for enabling the effect
238      */
239     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
240     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setEnabled(boolean enabled)241     public void setEnabled(boolean enabled) {
242         try {
243             mAm.getService().setSpatializerEnabled(enabled);
244         } catch (RemoteException e) {
245             Log.e(TAG, "Error calling setSpatializerEnabled", e);
246         }
247     }
248 
249     /**
250      * An interface to be notified of changes to the state of the spatializer effect.
251      */
252     public interface OnSpatializerStateChangedListener {
253         /**
254          * Called when the enabled state of the spatializer effect changes
255          * @param spat the {@code Spatializer} instance whose state changed
256          * @param enabled {@code true} if the spatializer effect is enabled on the device,
257          *                            {@code false} otherwise
258          * @see #isEnabled()
259          */
onSpatializerEnabledChanged(@onNull Spatializer spat, boolean enabled)260         void onSpatializerEnabledChanged(@NonNull Spatializer spat, boolean enabled);
261 
262         /**
263          * Called when the availability of the spatializer effect changes
264          * @param spat the {@code Spatializer} instance whose state changed
265          * @param available {@code true} if the spatializer effect is available and capable
266          *                  of processing the audio for the current configuration of the device,
267          *                  {@code false} otherwise.
268          * @see #isAvailable()
269          */
onSpatializerAvailableChanged(@onNull Spatializer spat, boolean available)270         void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available);
271     }
272 
273     /**
274      * @hide
275      * An interface to be notified of changes to the head tracking mode, used by the spatializer
276      * effect.
277      * Changes to the mode may come from explicitly setting a different mode
278      * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see
279      * {@link #getHeadTrackingMode()}
280      * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener)
281      * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener)
282      */
283     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
284     public interface OnHeadTrackingModeChangedListener {
285         /**
286          * Called when the actual head tracking mode of the spatializer changed.
287          * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing
288          * @param mode the new head tracking mode
289          */
onHeadTrackingModeChanged(@onNull Spatializer spatializer, @HeadTrackingMode int mode)290         void onHeadTrackingModeChanged(@NonNull Spatializer spatializer,
291                 @HeadTrackingMode int mode);
292 
293         /**
294          * Called when the desired head tracking mode of the spatializer changed
295          * @param spatializer the {@code Spatializer} instance whose head tracking mode was set
296          * @param mode the newly set head tracking mode
297          */
onDesiredHeadTrackingModeChanged(@onNull Spatializer spatializer, @HeadTrackingModeSet int mode)298         void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer,
299                 @HeadTrackingModeSet int mode);
300     }
301 
302 
303     /**
304      * @hide
305      * An interface to be notified of changes to the output stream used by the spatializer
306      * effect.
307      * @see #getOutput()
308      */
309     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
310     public interface OnSpatializerOutputChangedListener {
311         /**
312          * Called when the id of the output stream of the spatializer effect changed.
313          * @param spatializer the {@code Spatializer} instance whose output is updated
314          * @param output the id of the output stream, or 0 when there is no spatializer output
315          */
onSpatializerOutputChanged(@onNull Spatializer spatializer, @IntRange(from = 0) int output)316         void onSpatializerOutputChanged(@NonNull Spatializer spatializer,
317                 @IntRange(from = 0) int output);
318     }
319 
320     /**
321      * @hide
322      * An interface to be notified of updates to the head to soundstage pose, as represented by the
323      * current head tracking mode.
324      * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
325      */
326     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
327     public interface OnHeadToSoundstagePoseUpdatedListener {
328         /**
329          * Called when the head to soundstage transform is updated
330          * @param spatializer the {@code Spatializer} instance affected by the pose update
331          * @param pose the new pose data representing the transform between the frame
332          *                 of reference for the current head tracking mode (see
333          *                 {@link #getHeadTrackingMode()}) and the device being tracked (for
334          *                 instance a pair of headphones with a head tracker).<br>
335          *                 The head pose data is represented as an array of six float values, where
336          *                 the first three values are the translation vector, and the next three
337          *                 are the rotation vector.
338          */
onHeadToSoundstagePoseUpdated(@onNull Spatializer spatializer, @NonNull float[] pose)339         void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer,
340                 @NonNull float[] pose);
341     }
342 
343     /**
344      * Returns whether audio of the given {@link AudioFormat}, played with the given
345      * {@link AudioAttributes} can be spatialized.
346      * Note that the result reflects the capabilities of the device and may change when
347      * audio accessories are connected/disconnected (e.g. wired headphones plugged in or not).
348      * The result is independent from whether spatialization processing is enabled or not.
349      * @param attributes the {@code AudioAttributes} of the content as used for playback
350      * @param format the {@code AudioFormat} of the content as used for playback
351      * @return {@code true} if the device is capable of spatializing the combination of audio format
352      *     and attributes, {@code false} otherwise.
353      */
canBeSpatialized( @onNull AudioAttributes attributes, @NonNull AudioFormat format)354     public boolean canBeSpatialized(
355             @NonNull AudioAttributes attributes, @NonNull AudioFormat format) {
356         try {
357             return mAm.getService().canBeSpatialized(
358                     Objects.requireNonNull(attributes), Objects.requireNonNull(format));
359         } catch (RemoteException e) {
360             Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes
361                     + " format:" + format + " returning false", e);
362             return false;
363         }
364     }
365 
366     /**
367      * Adds a listener to be notified of changes to the enabled state of the
368      * {@code Spatializer}.
369      * @param executor the {@code Executor} handling the callback
370      * @param listener the listener to receive enabled state updates
371      * @see #isEnabled()
372      */
addOnSpatializerStateChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnSpatializerStateChangedListener listener)373     public void addOnSpatializerStateChangedListener(
374             @NonNull @CallbackExecutor Executor executor,
375             @NonNull OnSpatializerStateChangedListener listener) {
376         Objects.requireNonNull(executor);
377         Objects.requireNonNull(listener);
378         synchronized (mStateListenerLock) {
379             if (hasSpatializerStateListener(listener)) {
380                 throw new IllegalArgumentException(
381                         "Called addOnSpatializerStateChangedListener() "
382                         + "on a previously registered listener");
383             }
384             // lazy initialization of the list of strategy-preferred device listener
385             if (mStateListeners == null) {
386                 mStateListeners = new ArrayList<>();
387             }
388             mStateListeners.add(new StateListenerInfo(listener, executor));
389             if (mStateListeners.size() == 1) {
390                 // register binder for callbacks
391                 if (mInfoDispatcherStub == null) {
392                     mInfoDispatcherStub =
393                             new SpatializerInfoDispatcherStub();
394                 }
395                 try {
396                     mAm.getService().registerSpatializerCallback(
397                             mInfoDispatcherStub);
398                 } catch (RemoteException e) {
399                     throw e.rethrowFromSystemServer();
400                 }
401             }
402         }
403     }
404 
405     /**
406      * Removes a previously added listener for changes to the enabled state of the
407      * {@code Spatializer}.
408      * @param listener the listener to receive enabled state updates
409      * @see #isEnabled()
410      */
removeOnSpatializerStateChangedListener( @onNull OnSpatializerStateChangedListener listener)411     public void removeOnSpatializerStateChangedListener(
412             @NonNull OnSpatializerStateChangedListener listener) {
413         Objects.requireNonNull(listener);
414         synchronized (mStateListenerLock) {
415             if (!removeStateListener(listener)) {
416                 throw new IllegalArgumentException(
417                         "Called removeOnSpatializerStateChangedListener() "
418                         + "on an unregistered listener");
419             }
420             if (mStateListeners.size() == 0) {
421                 // unregister binder for callbacks
422                 try {
423                     mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub);
424                 } catch (RemoteException e) {
425                     throw e.rethrowFromSystemServer();
426                 } finally {
427                     mInfoDispatcherStub = null;
428                     mStateListeners = null;
429                 }
430             }
431         }
432     }
433 
434     /**
435      * @hide
436      * Returns the list of playback devices that are compatible with the playback of multichannel
437      * audio through virtualization
438      * @return a list of devices. An empty list indicates virtualization is not supported.
439      */
440     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
441     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getCompatibleAudioDevices()442     public @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
443         try {
444             return mAm.getService().getSpatializerCompatibleAudioDevices();
445         } catch (RemoteException e) {
446             Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), "
447                     + " returning empty list", e);
448             return new ArrayList<AudioDeviceAttributes>(0);
449         }
450     }
451 
452     /**
453      * @hide
454      * Adds a playback device to the list of devices compatible with the playback of multichannel
455      * audio through spatialization.
456      * @see #getCompatibleAudioDevices()
457      * @param ada the audio device compatible with spatialization
458      */
459     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
460     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
addCompatibleAudioDevice(@onNull AudioDeviceAttributes ada)461     public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
462         try {
463             mAm.getService().addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada));
464         } catch (RemoteException e) {
465             Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice(), ", e);
466         }
467     }
468 
469     /**
470      * @hide
471      * Remove a playback device from the list of devices compatible with the playback of
472      * multichannel audio through spatialization.
473      * @see #getCompatibleAudioDevices()
474      * @param ada the audio device incompatible with spatialization
475      */
476     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
477     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
removeCompatibleAudioDevice(@onNull AudioDeviceAttributes ada)478     public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
479         try {
480             mAm.getService().removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada));
481         } catch (RemoteException e) {
482             Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice(), ", e);
483         }
484     }
485 
486     private final Object mStateListenerLock = new Object();
487     /**
488      * List of listeners for state listener and their associated Executor.
489      * List is lazy-initialized on first registration
490      */
491     @GuardedBy("mStateListenerLock")
492     private @Nullable ArrayList<StateListenerInfo> mStateListeners;
493 
494     @GuardedBy("mStateListenerLock")
495     private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub;
496 
497     private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub {
498         @Override
dispatchSpatializerEnabledChanged(boolean enabled)499         public void dispatchSpatializerEnabledChanged(boolean enabled) {
500             // make a shallow copy of listeners so callback is not executed under lock
501             final ArrayList<StateListenerInfo> stateListeners;
502             synchronized (mStateListenerLock) {
503                 if (mStateListeners == null || mStateListeners.size() == 0) {
504                     return;
505                 }
506                 stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone();
507             }
508             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
509                 for (StateListenerInfo info : stateListeners) {
510                     info.mExecutor.execute(() ->
511                             info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled));
512                 }
513             }
514         }
515 
516         @Override
dispatchSpatializerAvailableChanged(boolean available)517         public void dispatchSpatializerAvailableChanged(boolean available) {
518             // make a shallow copy of listeners so callback is not executed under lock
519             final ArrayList<StateListenerInfo> stateListeners;
520             synchronized (mStateListenerLock) {
521                 if (mStateListeners == null || mStateListeners.size() == 0) {
522                     return;
523                 }
524                 stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone();
525             }
526             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
527                 for (StateListenerInfo info : stateListeners) {
528                     info.mExecutor.execute(() ->
529                             info.mListener.onSpatializerAvailableChanged(
530                                     Spatializer.this, available));
531                 }
532             }
533         }
534     }
535 
536     private static class StateListenerInfo {
537         final @NonNull OnSpatializerStateChangedListener mListener;
538         final @NonNull Executor mExecutor;
539 
StateListenerInfo(@onNull OnSpatializerStateChangedListener listener, @NonNull Executor exe)540         StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener,
541                 @NonNull Executor exe) {
542             mListener = listener;
543             mExecutor = exe;
544         }
545     }
546 
547     @GuardedBy("mStateListenerLock")
hasSpatializerStateListener(OnSpatializerStateChangedListener listener)548     private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) {
549         return getStateListenerInfo(listener) != null;
550     }
551 
552     @GuardedBy("mStateListenerLock")
getStateListenerInfo( OnSpatializerStateChangedListener listener)553     private @Nullable StateListenerInfo getStateListenerInfo(
554             OnSpatializerStateChangedListener listener) {
555         if (mStateListeners == null) {
556             return null;
557         }
558         for (StateListenerInfo info : mStateListeners) {
559             if (info.mListener == listener) {
560                 return info;
561             }
562         }
563         return null;
564     }
565 
566     @GuardedBy("mStateListenerLock")
567     /**
568      * @return true if the listener was removed from the list
569      */
removeStateListener(OnSpatializerStateChangedListener listener)570     private boolean removeStateListener(OnSpatializerStateChangedListener listener) {
571         final StateListenerInfo infoToRemove = getStateListenerInfo(listener);
572         if (infoToRemove != null) {
573             mStateListeners.remove(infoToRemove);
574             return true;
575         }
576         return false;
577     }
578 
579 
580     /**
581      * @hide
582      * Return the current head tracking mode as used by the system.
583      * Note this may differ from the desired head tracking mode. Reasons for the two to differ
584      * include: a head tracking device is not available for the current audio output device,
585      * the transmission conditions between the tracker and device have deteriorated and tracking
586      * has been disabled.
587      * @see #getDesiredHeadTrackingMode()
588      * @return the current head tracking mode
589      */
590     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
591     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getHeadTrackingMode()592     public @HeadTrackingMode int getHeadTrackingMode() {
593         try {
594             return mAm.getService().getActualHeadTrackingMode();
595         } catch (RemoteException e) {
596             Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
597             return HEAD_TRACKING_MODE_UNSUPPORTED;
598         }
599 
600     }
601 
602     /**
603      * @hide
604      * Return the desired head tracking mode.
605      * Note this may differ from the actual head tracking mode, reflected by
606      * {@link #getHeadTrackingMode()}.
607      * @return the desired head tring mode
608      */
609     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
610     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getDesiredHeadTrackingMode()611     public @HeadTrackingMode int getDesiredHeadTrackingMode() {
612         try {
613             return mAm.getService().getDesiredHeadTrackingMode();
614         } catch (RemoteException e) {
615             Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e);
616             return HEAD_TRACKING_MODE_UNSUPPORTED;
617         }
618     }
619 
620     /**
621      * @hide
622      * Returns the list of supported head tracking modes.
623      * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to
624      *         enable head tracking. The list will be empty if {@link #getHeadTrackingMode()}
625      *         is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be
626      *         {@link #HEAD_TRACKING_MODE_OTHER},
627      *         {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or
628      *         {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE}
629      */
630     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
631     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getSupportedHeadTrackingModes()632     public @NonNull List<Integer> getSupportedHeadTrackingModes() {
633         try {
634             final int[] modes = mAm.getService().getSupportedHeadTrackingModes();
635             final ArrayList<Integer> list = new ArrayList<>(0);
636             for (int mode : modes) {
637                 list.add(mode);
638             }
639             return list;
640         } catch (RemoteException e) {
641             Log.e(TAG, "Error calling getSupportedHeadTrackModes", e);
642             return new ArrayList(0);
643         }
644     }
645 
646     /**
647      * @hide
648      * Sets the desired head tracking mode.
649      * Note a set desired mode may differ from the actual head tracking mode.
650      * @see #getHeadTrackingMode()
651      * @param mode the desired head tracking mode, one of the values returned by
652      *             {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to
653      *             disable head tracking.
654      */
655     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
656     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setDesiredHeadTrackingMode(@eadTrackingModeSet int mode)657     public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) {
658         try {
659             mAm.getService().setDesiredHeadTrackingMode(mode);
660         } catch (RemoteException e) {
661             Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e);
662         }
663     }
664 
665     /**
666      * @hide
667      * Recenters the head tracking at the current position / orientation.
668      */
669     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
670     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
recenterHeadTracker()671     public void recenterHeadTracker() {
672         try {
673             mAm.getService().recenterHeadTracker();
674         } catch (RemoteException e) {
675             Log.e(TAG, "Error calling recenterHeadTracker", e);
676         }
677     }
678 
679     /**
680      * @hide
681      * Adds a listener to be notified of changes to the head tracking mode of the
682      * {@code Spatializer}
683      * @param executor the {@code Executor} handling the callbacks
684      * @param listener the listener to register
685      */
686     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
687     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
addOnHeadTrackingModeChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnHeadTrackingModeChangedListener listener)688     public void addOnHeadTrackingModeChangedListener(
689             @NonNull @CallbackExecutor Executor executor,
690             @NonNull OnHeadTrackingModeChangedListener listener) {
691         Objects.requireNonNull(executor);
692         Objects.requireNonNull(listener);
693         synchronized (mHeadTrackingListenerLock) {
694             if (hasListener(listener, mHeadTrackingListeners)) {
695                 throw new IllegalArgumentException(
696                         "Called addOnHeadTrackingModeChangedListener() "
697                                 + "on a previously registered listener");
698             }
699             // lazy initialization of the list of strategy-preferred device listener
700             if (mHeadTrackingListeners == null) {
701                 mHeadTrackingListeners = new ArrayList<>();
702             }
703             mHeadTrackingListeners.add(
704                     new ListenerInfo<OnHeadTrackingModeChangedListener>(listener, executor));
705             if (mHeadTrackingListeners.size() == 1) {
706                 // register binder for callbacks
707                 if (mHeadTrackingDispatcherStub == null) {
708                     mHeadTrackingDispatcherStub =
709                             new SpatializerHeadTrackingDispatcherStub();
710                 }
711                 try {
712                     mAm.getService().registerSpatializerHeadTrackingCallback(
713                             mHeadTrackingDispatcherStub);
714                 } catch (RemoteException e) {
715                     throw e.rethrowFromSystemServer();
716                 }
717             }
718         }
719     }
720 
721     /**
722      * @hide
723      * Removes a previously added listener for changes to the head tracking mode of the
724      * {@code Spatializer}.
725      * @param listener the listener to unregister
726      */
727     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
728     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
removeOnHeadTrackingModeChangedListener( @onNull OnHeadTrackingModeChangedListener listener)729     public void removeOnHeadTrackingModeChangedListener(
730             @NonNull OnHeadTrackingModeChangedListener listener) {
731         Objects.requireNonNull(listener);
732         synchronized (mHeadTrackingListenerLock) {
733             if (!removeListener(listener, mHeadTrackingListeners)) {
734                 throw new IllegalArgumentException(
735                         "Called removeOnHeadTrackingModeChangedListener() "
736                                 + "on an unregistered listener");
737             }
738             if (mHeadTrackingListeners.size() == 0) {
739                 // unregister binder for callbacks
740                 try {
741                     mAm.getService().unregisterSpatializerHeadTrackingCallback(
742                             mHeadTrackingDispatcherStub);
743                 } catch (RemoteException e) {
744                     throw e.rethrowFromSystemServer();
745                 } finally {
746                     mHeadTrackingDispatcherStub = null;
747                     mHeadTrackingListeners = null;
748                 }
749             }
750         }
751     }
752 
753     /**
754      * @hide
755      * Set the listener to receive head to soundstage pose updates.
756      * @param executor the {@code Executor} handling the callbacks
757      * @param listener the listener to register
758      * @see #clearOnHeadToSoundstagePoseUpdatedListener()
759      */
760     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
761     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setOnHeadToSoundstagePoseUpdatedListener( @onNull @allbackExecutor Executor executor, @NonNull OnHeadToSoundstagePoseUpdatedListener listener)762     public void setOnHeadToSoundstagePoseUpdatedListener(
763             @NonNull @CallbackExecutor Executor executor,
764             @NonNull OnHeadToSoundstagePoseUpdatedListener listener) {
765         Objects.requireNonNull(executor);
766         Objects.requireNonNull(listener);
767         synchronized (mPoseListenerLock) {
768             if (mPoseListener != null) {
769                 throw new IllegalStateException("Trying to overwrite existing listener");
770             }
771             mPoseListener =
772                     new ListenerInfo<OnHeadToSoundstagePoseUpdatedListener>(listener, executor);
773             mPoseDispatcher = new SpatializerPoseDispatcherStub();
774             try {
775                 mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher);
776             } catch (RemoteException e) {
777                 mPoseListener = null;
778                 mPoseDispatcher = null;
779             }
780         }
781     }
782 
783     /**
784      * @hide
785      * Clears the listener for head to soundstage pose updates
786      * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
787      */
788     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
789     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
clearOnHeadToSoundstagePoseUpdatedListener()790     public void clearOnHeadToSoundstagePoseUpdatedListener() {
791         synchronized (mPoseListenerLock) {
792             if (mPoseDispatcher == null) {
793                 throw (new IllegalStateException("No listener to clear"));
794             }
795             try {
796                 mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher);
797             } catch (RemoteException e) { }
798             mPoseListener = null;
799             mPoseDispatcher = null;
800         }
801     }
802 
803     /**
804      * @hide
805      * Sets an additional transform over the soundstage.
806      * The transform represents the pose of the soundstage, relative
807      * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in
808      * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in
809      * {@link #HEAD_TRACKING_MODE_DISABLED} mode).
810      * @param transform an array of 6 float values, the first 3 are the translation vector, the
811      *                  other 3 are the rotation vector.
812      */
813     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
814     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setGlobalTransform(@onNull float[] transform)815     public void setGlobalTransform(@NonNull float[] transform) {
816         if (Objects.requireNonNull(transform).length != 6) {
817             throw new IllegalArgumentException("transform array must be of size 6, was "
818                     + transform.length);
819         }
820         try {
821             mAm.getService().setSpatializerGlobalTransform(transform);
822         } catch (RemoteException e) {
823             Log.e(TAG, "Error calling setGlobalTransform", e);
824         }
825     }
826 
827     /**
828      * @hide
829      * Sets a parameter on the platform spatializer effect implementation.
830      * This is to be used for vendor-specific configurations of their effect, keys and values are
831      * not reuseable across implementations.
832      * @param key the parameter to change
833      * @param value an array for the value of the parameter to change
834      */
835     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
836     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setEffectParameter(int key, @NonNull byte[] value)837     public void setEffectParameter(int key, @NonNull byte[] value) {
838         Objects.requireNonNull(value);
839         try {
840             mAm.getService().setSpatializerParameter(key, value);
841         } catch (RemoteException e) {
842             Log.e(TAG, "Error calling setEffectParameter", e);
843         }
844     }
845 
846     /**
847      * @hide
848      * Retrieves a parameter value from the platform spatializer effect implementation.
849      * This is to be used for vendor-specific configurations of their effect, keys and values are
850      * not reuseable across implementations.
851      * @param key the parameter for which the value is queried
852      * @param value a non-empty array to contain the return value. The caller is responsible for
853      *              passing an array of size matching the parameter.
854      */
855     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
856     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getEffectParameter(int key, @NonNull byte[] value)857     public void getEffectParameter(int key, @NonNull byte[] value) {
858         Objects.requireNonNull(value);
859         try {
860             mAm.getService().getSpatializerParameter(key, value);
861         } catch (RemoteException e) {
862             Log.e(TAG, "Error calling getEffectParameter", e);
863         }
864     }
865 
866     /**
867      * @hide
868      * Returns the id of the output stream used for the spatializer effect playback
869      * @return id of the output stream, or 0 if no spatializer playback is active
870      */
871     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
872     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
getOutput()873     public @IntRange(from = 0) int getOutput() {
874         try {
875             return mAm.getService().getSpatializerOutput();
876         } catch (RemoteException e) {
877             Log.e(TAG, "Error calling getSpatializerOutput", e);
878             return 0;
879         }
880     }
881 
882     /**
883      * @hide
884      * Sets the listener to receive spatializer effect output updates
885      * @param executor the {@code Executor} handling the callbacks
886      * @param listener the listener to register
887      * @see #clearOnSpatializerOutputChangedListener()
888      * @see #getOutput()
889      */
890     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
891     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
setOnSpatializerOutputChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnSpatializerOutputChangedListener listener)892     public void setOnSpatializerOutputChangedListener(
893             @NonNull @CallbackExecutor Executor executor,
894             @NonNull OnSpatializerOutputChangedListener listener) {
895         Objects.requireNonNull(executor);
896         Objects.requireNonNull(listener);
897         synchronized (mOutputListenerLock) {
898             if (mOutputListener != null) {
899                 throw new IllegalStateException("Trying to overwrite existing listener");
900             }
901             mOutputListener =
902                     new ListenerInfo<OnSpatializerOutputChangedListener>(listener, executor);
903             mOutputDispatcher = new SpatializerOutputDispatcherStub();
904             try {
905                 mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher);
906             } catch (RemoteException e) {
907                 mOutputListener = null;
908                 mOutputDispatcher = null;
909             }
910         }
911     }
912 
913     /**
914      * @hide
915      * Clears the listener for spatializer effect output updates
916      * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener)
917      */
918     @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
919     @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
clearOnSpatializerOutputChangedListener()920     public void clearOnSpatializerOutputChangedListener() {
921         synchronized (mOutputListenerLock) {
922             if (mOutputDispatcher == null) {
923                 throw (new IllegalStateException("No listener to clear"));
924             }
925             try {
926                 mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher);
927             } catch (RemoteException e) { }
928             mOutputListener = null;
929             mOutputDispatcher = null;
930         }
931     }
932 
933     //-----------------------------------------------------------------------------
934     // callback helper definitions
935 
936     private static class ListenerInfo<T> {
937         final @NonNull T mListener;
938         final @NonNull Executor mExecutor;
939 
ListenerInfo(T listener, Executor exe)940         ListenerInfo(T listener, Executor exe) {
941             mListener = listener;
942             mExecutor = exe;
943         }
944     }
945 
getListenerInfo( T listener, ArrayList<ListenerInfo<T>> listeners)946     private static <T> ListenerInfo<T> getListenerInfo(
947             T listener, ArrayList<ListenerInfo<T>> listeners) {
948         if (listeners == null) {
949             return null;
950         }
951         for (ListenerInfo<T> info : listeners) {
952             if (info.mListener == listener) {
953                 return info;
954             }
955         }
956         return null;
957     }
958 
hasListener(T listener, ArrayList<ListenerInfo<T>> listeners)959     private static <T> boolean hasListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
960         return getListenerInfo(listener, listeners) != null;
961     }
962 
removeListener(T listener, ArrayList<ListenerInfo<T>> listeners)963     private static <T> boolean removeListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
964         final ListenerInfo<T> infoToRemove = getListenerInfo(listener, listeners);
965         if (infoToRemove != null) {
966             listeners.remove(infoToRemove);
967             return true;
968         }
969         return false;
970     }
971 
972     //-----------------------------------------------------------------------------
973     // head tracking callback management and stub
974 
975     private final Object mHeadTrackingListenerLock = new Object();
976     /**
977      * List of listeners for head tracking mode listener and their associated Executor.
978      * List is lazy-initialized on first registration
979      */
980     @GuardedBy("mHeadTrackingListenerLock")
981     private @Nullable ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>
982             mHeadTrackingListeners;
983 
984     @GuardedBy("mHeadTrackingListenerLock")
985     private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub;
986 
987     private final class SpatializerHeadTrackingDispatcherStub
988             extends ISpatializerHeadTrackingModeCallback.Stub {
989         @Override
dispatchSpatializerActualHeadTrackingModeChanged(int mode)990         public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) {
991             // make a shallow copy of listeners so callback is not executed under lock
992             final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
993             synchronized (mHeadTrackingListenerLock) {
994                 if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
995                     return;
996                 }
997                 headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
998                         mHeadTrackingListeners.clone();
999             }
1000             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
1001                 for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
1002                     info.mExecutor.execute(() -> info.mListener
1003                             .onHeadTrackingModeChanged(Spatializer.this, mode));
1004                 }
1005             }
1006         }
1007 
1008         @Override
dispatchSpatializerDesiredHeadTrackingModeChanged(int mode)1009         public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) {
1010             // make a shallow copy of listeners so callback is not executed under lock
1011             final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
1012             synchronized (mHeadTrackingListenerLock) {
1013                 if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
1014                     return;
1015                 }
1016                 headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
1017                         mHeadTrackingListeners.clone();
1018             }
1019             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
1020                 for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
1021                     info.mExecutor.execute(() -> info.mListener
1022                             .onDesiredHeadTrackingModeChanged(Spatializer.this, mode));
1023                 }
1024             }
1025         }
1026     }
1027 
1028     //-----------------------------------------------------------------------------
1029     // head pose callback management and stub
1030     private final Object mPoseListenerLock = new Object();
1031     /**
1032      * Listener for head to soundstage updates
1033      */
1034     @GuardedBy("mPoseListenerLock")
1035     private @Nullable ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> mPoseListener;
1036     @GuardedBy("mPoseListenerLock")
1037     private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher;
1038 
1039     private final class SpatializerPoseDispatcherStub
1040             extends ISpatializerHeadToSoundStagePoseCallback.Stub {
1041 
1042         @Override
dispatchPoseChanged(float[] pose)1043         public void dispatchPoseChanged(float[] pose) {
1044             // make a copy of ref to listener so callback is not executed under lock
1045             final ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> listener;
1046             synchronized (mPoseListenerLock) {
1047                 listener = mPoseListener;
1048             }
1049             if (listener == null) {
1050                 return;
1051             }
1052             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
1053                 listener.mExecutor.execute(() -> listener.mListener
1054                         .onHeadToSoundstagePoseUpdated(Spatializer.this, pose));
1055             }
1056         }
1057     }
1058 
1059     //-----------------------------------------------------------------------------
1060     // output callback management and stub
1061     private final Object mOutputListenerLock = new Object();
1062     /**
1063      * Listener for output updates
1064      */
1065     @GuardedBy("mOutputListenerLock")
1066     private @Nullable ListenerInfo<OnSpatializerOutputChangedListener> mOutputListener;
1067     @GuardedBy("mOutputListenerLock")
1068     private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher;
1069 
1070     private final class SpatializerOutputDispatcherStub
1071             extends ISpatializerOutputCallback.Stub {
1072 
1073         @Override
dispatchSpatializerOutputChanged(int output)1074         public void dispatchSpatializerOutputChanged(int output) {
1075             // make a copy of ref to listener so callback is not executed under lock
1076             final ListenerInfo<OnSpatializerOutputChangedListener> listener;
1077             synchronized (mOutputListenerLock) {
1078                 listener = mOutputListener;
1079             }
1080             if (listener == null) {
1081                 return;
1082             }
1083             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
1084                 listener.mExecutor.execute(() -> listener.mListener
1085                         .onSpatializerOutputChanged(Spatializer.this, output));
1086             }
1087         }
1088     }
1089 }
1090