Immersive audio APIs for headtracking

Headtracking:
- setter / getter and listener for headtracking mode
- listener for head pose
- global transform
- recentering

Bug: 191404931
Test: atest SpatializerHeadTrackingTest
Change-Id: I15cea62365584f84ae5a9149719902c671944f28
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0d3dd7c..9c01bca 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5375,9 +5375,33 @@
 
   public class Spatializer {
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addOnHeadTrackingModeChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnHeadToSoundstagePoseUpdatedListener();
     method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<android.media.AudioDeviceAttributes> getCompatibleAudioDevices();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getDesiredHeadTrackingMode();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getHeadTrackingMode();
+    method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<java.lang.Integer> getSupportedHeadTrackingModes();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void recenterHeadTracker();
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeOnHeadTrackingModeChangedListener(@NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setDesiredHeadTrackingMode(int);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEnabled(boolean);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setGlobalTransform(@NonNull float[]);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnHeadToSoundstagePoseUpdatedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadToSoundstagePoseUpdatedListener);
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_DISABLED = -1; // 0xffffffff
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_OTHER = 0; // 0x0
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; // 0x2
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; // 0x1
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; // 0xfffffffe
+  }
+
+  public static interface Spatializer.OnHeadToSoundstagePoseUpdatedListener {
+    method public void onHeadToSoundstagePoseUpdated(@NonNull android.media.Spatializer, @NonNull float[]);
+  }
+
+  public static interface Spatializer.OnHeadTrackingModeChangedListener {
+    method public void onDesiredHeadTrackingModeChanged(@NonNull android.media.Spatializer, int);
+    method public void onHeadTrackingModeChanged(@NonNull android.media.Spatializer, int);
   }
 
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 8480c52..bc10401 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -36,6 +36,8 @@
 import android.media.IRingtonePlayer;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
 import android.media.IVolumeController;
 import android.media.IVolumeController;
 import android.media.PlayerBase;
@@ -405,13 +407,33 @@
 
     boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af);
 
-    void registerSpatializerCallback(in ISpatializerCallback callback);
+    void registerSpatializerCallback(in ISpatializerCallback cb);
 
-    void unregisterSpatializerCallback(in ISpatializerCallback callback);
+    void unregisterSpatializerCallback(in ISpatializerCallback cb);
+
+    void registerSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb);
+
+    void unregisterSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb);
+
+    void registerHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb);
+
+    void unregisterHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb);
 
     List<AudioDeviceAttributes> getSpatializerCompatibleAudioDevices();
 
     void addSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada);
 
     void removeSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada);
+
+    void setDesiredHeadTrackingMode(int mode);
+
+    int getDesiredHeadTrackingMode();
+
+    int[] getSupportedHeadTrackingModes();
+
+    int getActualHeadTrackingMode();
+
+    oneway void setSpatializerGlobalTransform(in float[] transform);
+
+    oneway void recenterHeadTracker();
 }
diff --git a/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl
new file mode 100644
index 0000000..01a1465
--- /dev/null
+++ b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * AIDL for the AudioService to signal Spatializer state changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerHeadToSoundStagePoseCallback {
+
+    /**
+     * The pose is sent as an array of 6 float values, the first 3 are the translation vector, the
+     * other 3 are the rotation vector.
+     */
+    void dispatchPoseChanged(in float[] pose);
+}
diff --git a/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl
new file mode 100644
index 0000000..c61f86e
--- /dev/null
+++ b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * AIDL for the AudioService to signal Spatializer head tracking mode changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerHeadTrackingModeCallback {
+
+    void dispatchSpatializerActualHeadTrackingModeChanged(int mode);
+
+    void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode);
+}
diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java
index d8519b6..b062eea 100644
--- a/media/java/android/media/Spatializer.java
+++ b/media/java/android/media/Spatializer.java
@@ -49,21 +49,9 @@
 
     private final @NonNull AudioManager mAm;
 
-    private final Object mStateListenerLock = new Object();
-
     private static final String TAG = "Spatializer";
 
     /**
-     * List of listeners for state listener and their associated Executor.
-     * List is lazy-initialized on first registration
-     */
-    @GuardedBy("mStateListenerLock")
-    private @Nullable ArrayList<StateListenerInfo> mStateListeners;
-
-    @GuardedBy("mStateListenerLock")
-    private SpatializerInfoDispatcherStub mInfoDispatcherStub;
-
-    /**
      * @hide
      * Constructor with AudioManager acting as proxy to AudioService
      * @param am a non-null AudioManager
@@ -141,6 +129,75 @@
      */
     public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
 
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_UNSUPPORTED,
+            HEAD_TRACKING_MODE_DISABLED,
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingMode {};
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_DISABLED,
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingModeSet {};
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingModeSupported {};
+
+    /**
+     * @hide
+     * Constant indicating head tracking is not supported by this {@code Spatializer}
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is disabled on this {@code Spatializer}
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_DISABLED = -1;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an
+     * error state but represents a customized behavior not defined by this API.
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_OTHER = 0;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is tracking the user's position / orientation relative to
+     * the world around them
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is tracking the user's position / orientation relative to
+     * the device
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2;
+
     /**
      * Return the level of support for the spatialization feature on this device.
      * This level of support is independent of whether the {@code Spatializer} is currently
@@ -176,7 +233,7 @@
     }
 
     /**
-     * An interface to be notified of changes to the state of the spatializer.
+     * An interface to be notified of changes to the state of the spatializer effect.
      */
     public interface OnSpatializerStateChangedListener {
         /**
@@ -200,6 +257,58 @@
     }
 
     /**
+     * @hide
+     * An interface to be notified of changes to the head tracking mode, used by the spatializer
+     * effect.
+     * Changes to the mode may come from explicitly setting a different mode
+     * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see
+     * {@link #getHeadTrackingMode()}
+     * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener)
+     * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnHeadTrackingModeChangedListener {
+        /**
+         * Called when the actual head tracking mode of the spatializer changed.
+         * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing
+         * @param mode the new head tracking mode
+         */
+        void onHeadTrackingModeChanged(@NonNull Spatializer spatializer,
+                @HeadTrackingMode int mode);
+
+        /**
+         * Called when the desired head tracking mode of the spatializer changed
+         * @param spatializer the {@code Spatializer} instance whose head tracking mode was set
+         * @param mode the newly set head tracking mode
+         */
+        void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer,
+                @HeadTrackingModeSet int mode);
+    }
+
+    /**
+     * @hide
+     * An interface to be notified of updates to the head to soundstage pose, as represented by the
+     * current head tracking mode.
+     * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnHeadToSoundstagePoseUpdatedListener {
+        /**
+         * Called when the head to soundstage transform is updated
+         * @param spatializer the {@code Spatializer} instance affected by the pose update
+         * @param pose the new pose data representing the transform between the frame
+         *                 of reference for the current head tracking mode (see
+         *                 {@link #getHeadTrackingMode()}) and the device being tracked (for
+         *                 instance a pair of headphones with a head tracker).<br>
+         *                 The head pose data is represented as an array of six float values, where
+         *                 the first three values are the translation vector, and the next three
+         *                 are the rotation vector.
+         */
+        void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer,
+                @NonNull float[] pose);
+    }
+
+    /**
      * Returns whether audio of the given {@link AudioFormat}, played with the given
      * {@link AudioAttributes} can be spatialized.
      * Note that the result reflects the capabilities of the device and may change when
@@ -342,6 +451,17 @@
         }
     }
 
+    private final Object mStateListenerLock = new Object();
+    /**
+     * List of listeners for state listener and their associated Executor.
+     * List is lazy-initialized on first registration
+     */
+    @GuardedBy("mStateListenerLock")
+    private @Nullable ArrayList<StateListenerInfo> mStateListeners;
+
+    @GuardedBy("mStateListenerLock")
+    private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub;
+
     private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub {
         @Override
         public void dispatchSpatializerEnabledChanged(boolean enabled) {
@@ -423,4 +543,378 @@
         }
         return false;
     }
+
+
+    /**
+     * @hide
+     * Return the current head tracking mode as used by the system.
+     * Note this may differ from the desired head tracking mode. Reasons for the two to differ
+     * include: a head tracking device is not available for the current audio output device,
+     * the transmission conditions between the tracker and device have deteriorated and tracking
+     * has been disabled.
+     * @see #getDesiredHeadTrackingMode()
+     * @return the current head tracking mode
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @HeadTrackingMode int getHeadTrackingMode() {
+        try {
+            return mAm.getService().getActualHeadTrackingMode();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
+            return HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+
+    }
+
+    /**
+     * @hide
+     * Return the desired head tracking mode.
+     * Note this may differ from the actual head tracking mode, reflected by
+     * {@link #getHeadTrackingMode()}.
+     * @return the desired head tring mode
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @HeadTrackingMode int getDesiredHeadTrackingMode() {
+        try {
+            return mAm.getService().getDesiredHeadTrackingMode();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e);
+            return HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+    }
+
+    /**
+     * @hide
+     * Returns the list of supported head tracking modes.
+     * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to
+     *         enable head tracking. The list will be empty if {@link #getHeadTrackingMode()}
+     *         is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be
+     *         {@link #HEAD_TRACKING_MODE_OTHER},
+     *         {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or
+     *         {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE}
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @NonNull List<Integer> getSupportedHeadTrackingModes() {
+        try {
+            final int[] modes = mAm.getService().getSupportedHeadTrackingModes();
+            final ArrayList<Integer> list = new ArrayList<>(0);
+            for (int mode : modes) {
+                list.add(mode);
+            }
+            return list;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSupportedHeadTrackModes", e);
+            return new ArrayList(0);
+        }
+    }
+
+    /**
+     * @hide
+     * Sets the desired head tracking mode.
+     * Note a set desired mode may differ from the actual head tracking mode.
+     * @see #getHeadTrackingMode()
+     * @param mode the desired head tracking mode, one of the values returned by
+     *             {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to
+     *             disable head tracking.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) {
+        try {
+            mAm.getService().setDesiredHeadTrackingMode(mode);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e);
+        }
+    }
+
+    /**
+     * @hide
+     * Recenters the head tracking at the current position / orientation.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void recenterHeadTracker() {
+        try {
+            mAm.getService().recenterHeadTracker();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling recenterHeadTracker", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Adds a listener to be notified of changes to the head tracking mode of the
+     * {@code Spatializer}
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void addOnHeadTrackingModeChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnHeadTrackingModeChangedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mHeadTrackingListenerLock) {
+            if (hasListener(listener, mHeadTrackingListeners)) {
+                throw new IllegalArgumentException(
+                        "Called addOnHeadTrackingModeChangedListener() "
+                                + "on a previously registered listener");
+            }
+            // lazy initialization of the list of strategy-preferred device listener
+            if (mHeadTrackingListeners == null) {
+                mHeadTrackingListeners = new ArrayList<>();
+            }
+            mHeadTrackingListeners.add(
+                    new ListenerInfo<OnHeadTrackingModeChangedListener>(listener, executor));
+            if (mHeadTrackingListeners.size() == 1) {
+                // register binder for callbacks
+                if (mHeadTrackingDispatcherStub == null) {
+                    mHeadTrackingDispatcherStub =
+                            new SpatializerHeadTrackingDispatcherStub();
+                }
+                try {
+                    mAm.getService().registerSpatializerHeadTrackingCallback(
+                            mHeadTrackingDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Removes a previously added listener for changes to the head tracking mode of the
+     * {@code Spatializer}.
+     * @param listener the listener to unregister
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void removeOnHeadTrackingModeChangedListener(
+            @NonNull OnHeadTrackingModeChangedListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mHeadTrackingListenerLock) {
+            if (!removeListener(listener, mHeadTrackingListeners)) {
+                throw new IllegalArgumentException(
+                        "Called removeOnHeadTrackingModeChangedListener() "
+                                + "on an unregistered listener");
+            }
+            if (mHeadTrackingListeners.size() == 0) {
+                // unregister binder for callbacks
+                try {
+                    mAm.getService().unregisterSpatializerHeadTrackingCallback(
+                            mHeadTrackingDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                } finally {
+                    mHeadTrackingDispatcherStub = null;
+                    mHeadTrackingListeners = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Set the listener to receive head to soundstage pose updates.
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     * @see #clearOnHeadToSoundstagePoseUpdatedListener()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setOnHeadToSoundstagePoseUpdatedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnHeadToSoundstagePoseUpdatedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mPoseListenerLock) {
+            if (mPoseListener != null) {
+                throw new IllegalStateException("Trying to overwrite existing listener");
+            }
+            mPoseListener =
+                    new ListenerInfo<OnHeadToSoundstagePoseUpdatedListener>(listener, executor);
+            mPoseDispatcher = new SpatializerPoseDispatcherStub();
+            try {
+                mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher);
+            } catch (RemoteException e) {
+                mPoseListener = null;
+                mPoseDispatcher = null;
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Clears the listener for head to soundstage pose updates
+     * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void clearOnHeadToSoundstagePoseUpdatedListener() {
+        synchronized (mPoseListenerLock) {
+            if (mPoseDispatcher == null) {
+                throw (new IllegalStateException("No listener to clear"));
+            }
+            try {
+                mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher);
+            } catch (RemoteException e) { }
+            mPoseListener = null;
+            mPoseDispatcher = null;
+        }
+    }
+
+    /**
+     * @hide
+     * Sets an additional transform over the soundstage.
+     * The transform represents the pose of the soundstage, relative
+     * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in
+     * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in
+     * {@link #HEAD_TRACKING_MODE_DISABLED} mode).
+     * @param transform an array of 6 float values, the first 3 are the translation vector, the
+     *                  other 3 are the rotation vector.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setGlobalTransform(@NonNull float[] transform) {
+        if (Objects.requireNonNull(transform).length != 6) {
+            throw new IllegalArgumentException("transform array must be of size 6, was "
+                    + transform.length);
+        }
+        try {
+            mAm.getService().setSpatializerGlobalTransform(transform);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setGlobalTransform", e);
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // callback helper definitions
+
+    private static class ListenerInfo<T> {
+        final @NonNull T mListener;
+        final @NonNull Executor mExecutor;
+
+        ListenerInfo(T listener, Executor exe) {
+            mListener = listener;
+            mExecutor = exe;
+        }
+    }
+
+    private static <T> ListenerInfo<T> getListenerInfo(
+            T listener, ArrayList<ListenerInfo<T>> listeners) {
+        if (listeners == null) {
+            return null;
+        }
+        for (ListenerInfo<T> info : listeners) {
+            if (info.mListener == listener) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    private static <T> boolean hasListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
+        return getListenerInfo(listener, listeners) != null;
+    }
+
+    private static <T> boolean removeListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
+        final ListenerInfo<T> infoToRemove = getListenerInfo(listener, listeners);
+        if (infoToRemove != null) {
+            listeners.remove(infoToRemove);
+            return true;
+        }
+        return false;
+    }
+
+    //-----------------------------------------------------------------------------
+    // head tracking callback management and stub
+
+    private final Object mHeadTrackingListenerLock = new Object();
+    /**
+     * List of listeners for head tracking mode listener and their associated Executor.
+     * List is lazy-initialized on first registration
+     */
+    @GuardedBy("mHeadTrackingListenerLock")
+    private @Nullable ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>
+            mHeadTrackingListeners;
+
+    @GuardedBy("mHeadTrackingListenerLock")
+    private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub;
+
+    private final class SpatializerHeadTrackingDispatcherStub
+            extends ISpatializerHeadTrackingModeCallback.Stub {
+        @Override
+        public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
+            synchronized (mHeadTrackingListenerLock) {
+                if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
+                    return;
+                }
+                headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
+                        mHeadTrackingListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
+                    info.mExecutor.execute(() -> info.mListener
+                            .onHeadTrackingModeChanged(Spatializer.this, mode));
+                }
+            }
+        }
+
+        @Override
+        public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
+            synchronized (mHeadTrackingListenerLock) {
+                if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
+                    return;
+                }
+                headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
+                        mHeadTrackingListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
+                    info.mExecutor.execute(() -> info.mListener
+                            .onDesiredHeadTrackingModeChanged(Spatializer.this, mode));
+                }
+            }
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // head pose callback management and stub
+    private final Object mPoseListenerLock = new Object();
+    /**
+     * Listener for head to soundstage updates
+     */
+    @GuardedBy("mPoseListenerLock")
+    private @Nullable ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> mPoseListener;
+    @GuardedBy("mPoseListenerLock")
+    private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher;
+
+    private final class SpatializerPoseDispatcherStub
+            extends ISpatializerHeadToSoundStagePoseCallback.Stub {
+
+        @Override
+        public void dispatchPoseChanged(float[] pose) {
+            // make a copy of ref to listener so callback is not executed under lock
+            final ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> listener;
+            synchronized (mPoseListenerLock) {
+                listener = mPoseListener;
+            }
+            if (listener == null) {
+                return;
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                listener.mExecutor.execute(() -> listener.mListener
+                        .onHeadToSoundstagePoseUpdated(Spatializer.this, pose));
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b230200..5d0bad2 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -94,6 +94,8 @@
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
 import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IVolumeController;
 import android.media.MediaMetrics;
@@ -8322,16 +8324,48 @@
 
     /** @see Spatializer.SpatializerInfoDispatcherStub */
     public void registerSpatializerCallback(
-            @NonNull ISpatializerCallback dispatcher) {
-        Objects.requireNonNull(dispatcher);
-        mSpatializerHelper.registerStateCallback(dispatcher);
+            @NonNull ISpatializerCallback cb) {
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerStateCallback(cb);
     }
 
     /** @see Spatializer.SpatializerInfoDispatcherStub */
     public void unregisterSpatializerCallback(
-            @NonNull ISpatializerCallback dispatcher) {
-        Objects.requireNonNull(dispatcher);
-        mSpatializerHelper.unregisterStateCallback(dispatcher);
+            @NonNull ISpatializerCallback cb) {
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterStateCallback(cb);
+    }
+
+    /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
+    public void registerSpatializerHeadTrackingCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerHeadTrackingModeCallback(cb);
+    }
+
+    /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
+    public void unregisterSpatializerHeadTrackingCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterHeadTrackingModeCallback(cb);
+    }
+
+    /** @see Spatializer#setOnHeadToSoundstagePoseUpdatedListener */
+    public void registerHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerHeadToSoundstagePoseCallback(cb);
+    }
+
+    /** @see Spatializer#clearOnHeadToSoundstagePoseUpdatedListener */
+    public void unregisterHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterHeadToSoundstagePoseCallback(cb);
     }
 
     /** @see Spatializer#getSpatializerCompatibleAudioDevices() */
@@ -8354,6 +8388,51 @@
         mSpatializerHelper.removeCompatibleAudioDevice(ada);
     }
 
+    /** @see Spatializer#getSupportedHeadTrackingModes() */
+    public int[] getSupportedHeadTrackingModes() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getSupportedHeadTrackingModes();
+    }
+
+    /** @see Spatializer#getHeadTrackingMode() */
+    public int getActualHeadTrackingMode() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getActualHeadTrackingMode();
+    }
+
+    /** @see Spatializer#getDesiredHeadTrackingMode() */
+    public int getDesiredHeadTrackingMode() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getDesiredHeadTrackingMode();
+    }
+
+    /** @see Spatializer#setGlobalTransform */
+    public void setSpatializerGlobalTransform(@NonNull float[] transform) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(transform);
+        mSpatializerHelper.setGlobalTransform(transform);
+    }
+
+    /** @see Spatializer#recenterHeadTracker() */
+    public void recenterHeadTracker() {
+        enforceModifyDefaultAudioEffectsPermission();
+        mSpatializerHelper.recenterHeadTracker();
+    }
+
+    /** @see Spatializer#setDesiredHeadTrackingMode */
+    public void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
+        enforceModifyDefaultAudioEffectsPermission();
+        switch(mode) {
+            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+                break;
+            default:
+                return;
+        }
+        mSpatializerHelper.setDesiredHeadTrackingMode(mode);
+    }
+
     //==========================================================================================
     private boolean readCameraSoundForced() {
         return SystemProperties.getBoolean("audio.camerasound.force", false) ||
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 32ac785..1d85974 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -25,7 +25,10 @@
 import android.media.INativeSpatializerCallback;
 import android.media.ISpatializer;
 import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
 import android.media.Spatializer;
+import android.media.SpatializerHeadTrackingMode;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.util.Log;
@@ -64,6 +67,8 @@
     /** current level as reported by native Spatializer in callback */
     private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
     private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+    private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+    private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
     private @Nullable ISpatializer mSpat;
     private @Nullable SpatializerCallback mSpatCallback;
 
@@ -150,6 +155,7 @@
         mState = STATE_UNINITIALIZED;
         mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
         mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+        mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
         init();
         setFeatureEnabled(featureEnabled);
     }
@@ -189,6 +195,15 @@
 
         public void onHeadTrackingModeChanged(byte mode)  {
             logd("SpatializerCallback.onHeadTrackingModeChanged mode:" + mode);
+            int oldMode, newMode;
+            synchronized (this) {
+                oldMode = mActualHeadTrackingMode;
+                mActualHeadTrackingMode = headTrackingModeTypeToSpatializerInt(mode);
+                newMode = mActualHeadTrackingMode;
+            }
+            if (oldMode != newMode) {
+                dispatchActualHeadTrackingMode(newMode);
+            }
         }
 
         public void onHeadToSoundStagePoseUpdated(float[] headToStage)  {
@@ -196,6 +211,11 @@
                 Log.e(TAG, "SpatializerCallback.onHeadToStagePoseUpdated null transform");
                 return;
             }
+            if (headToStage.length != 6) {
+                Log.e(TAG, "SpatializerCallback.onHeadToStagePoseUpdated invalid transform length"
+                        + headToStage.length);
+                return;
+            }
             if (DEBUG) {
                 // 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters
                 StringBuilder t = new StringBuilder(42);
@@ -204,6 +224,7 @@
                 }
                 logd("SpatializerCallback.onHeadToStagePoseUpdated headToStage:" + t);
             }
+            dispatchPoseUpdate(headToStage);
         }
     };
 
@@ -472,4 +493,239 @@
         logd("canBeSpatialized returning " + able);
         return able;
     }
+
+    //------------------------------------------------------
+    // head tracking
+    final RemoteCallbackList<ISpatializerHeadTrackingModeCallback> mHeadTrackingModeCallbacks =
+            new RemoteCallbackList<ISpatializerHeadTrackingModeCallback>();
+
+    synchronized void registerHeadTrackingModeCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback callback) {
+        mHeadTrackingModeCallbacks.register(callback);
+    }
+
+    synchronized void unregisterHeadTrackingModeCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback callback) {
+        mHeadTrackingModeCallbacks.unregister(callback);
+    }
+
+    synchronized int[] getSupportedHeadTrackingModes() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+                return new int[0];
+            case STATE_NOT_SUPPORTED:
+                // return an empty list when Spatializer functionality is not supported
+                // because the list of head tracking modes you can set is actually empty
+                // as defined in {@link Spatializer#getSupportedHeadTrackingModes()}
+                return new int[0];
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    return new int[0];
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            final byte[] values = mSpat.getSupportedHeadTrackingModes();
+            ArrayList<Integer> list = new ArrayList<>(0);
+            for (byte value : values) {
+                switch (value) {
+                    case SpatializerHeadTrackingMode.OTHER:
+                    case SpatializerHeadTrackingMode.DISABLED:
+                        // not expected here, skip
+                        break;
+                    case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+                    case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+                        list.add(headTrackingModeTypeToSpatializerInt(value));
+                        break;
+                    default:
+                        Log.e(TAG, "Unexpected head tracking mode:" + value,
+                                new IllegalArgumentException("invalid mode"));
+                        break;
+                }
+            }
+            int[] modes = new int[list.size()];
+            for (int i = 0; i < list.size(); i++) {
+                modes[i] = list.get(i);
+            }
+            return modes;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSupportedHeadTrackingModes", e);
+            return new int[] { Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED };
+        }
+    }
+
+    synchronized int getActualHeadTrackingMode() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+            case STATE_NOT_SUPPORTED:
+                return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            return headTrackingModeTypeToSpatializerInt(mSpat.getActualHeadTrackingMode());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
+            return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+    }
+
+    synchronized int getDesiredHeadTrackingMode() {
+        return mDesiredHeadTrackingMode;
+    }
+
+    synchronized void setGlobalTransform(@NonNull float[] transform) {
+        if (transform.length != 6) {
+            throw new IllegalArgumentException("invalid array size" + transform.length);
+        }
+        if (!checkSpatForHeadTracking("setGlobalTransform")) {
+            return;
+        }
+        try {
+            mSpat.setGlobalTransform(transform);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setGlobalTransform", e);
+        }
+    }
+
+    synchronized void recenterHeadTracker() {
+        if (!checkSpatForHeadTracking("recenterHeadTracker")) {
+            return;
+        }
+        try {
+            mSpat.recenterHeadTracker();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling recenterHeadTracker", e);
+        }
+    }
+
+    synchronized void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
+        if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) {
+            return;
+        }
+        try {
+            if (mode != mDesiredHeadTrackingMode) {
+                mSpat.setDesiredHeadTrackingMode(spatializerIntToHeadTrackingModeType(mode));
+                mDesiredHeadTrackingMode = mode;
+                dispatchDesiredHeadTrackingMode(mode);
+            }
+
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setDesiredHeadTrackingMode", e);
+        }
+    }
+
+    private int headTrackingModeTypeToSpatializerInt(byte mode) {
+        switch (mode) {
+            case SpatializerHeadTrackingMode.OTHER:
+                return Spatializer.HEAD_TRACKING_MODE_OTHER;
+            case SpatializerHeadTrackingMode.DISABLED:
+                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+            case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
+            case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
+        }
+    }
+
+    private byte spatializerIntToHeadTrackingModeType(int sdkMode) {
+        switch (sdkMode) {
+            case Spatializer.HEAD_TRACKING_MODE_OTHER:
+                return SpatializerHeadTrackingMode.OTHER;
+            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+                return SpatializerHeadTrackingMode.DISABLED;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+                return SpatializerHeadTrackingMode.RELATIVE_WORLD;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+                return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
+        }
+    }
+
+    private boolean checkSpatForHeadTracking(String funcName) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                return false;
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer when calling " + funcName));
+                }
+                break;
+        }
+        return true;
+    }
+
+    private void dispatchActualHeadTrackingMode(int newMode) {
+        final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadTrackingModeCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerActualHeadTrackingModeChanged(newMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerActualHeadTrackingModeChanged", e);
+            }
+        }
+        mHeadTrackingModeCallbacks.finishBroadcast();
+    }
+
+    private void dispatchDesiredHeadTrackingMode(int newMode) {
+        final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadTrackingModeCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerDesiredHeadTrackingModeChanged(newMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerDesiredHeadTrackingModeChanged", e);
+            }
+        }
+        mHeadTrackingModeCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
+    // head pose
+    final RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback> mHeadPoseCallbacks =
+            new RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback>();
+
+    synchronized void registerHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
+        mHeadPoseCallbacks.register(callback);
+    }
+
+    synchronized void unregisterHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
+        mHeadPoseCallbacks.unregister(callback);
+    }
+
+    private void dispatchPoseUpdate(float[] pose) {
+        final int nbCallbacks = mHeadPoseCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadPoseCallbacks.getBroadcastItem(i)
+                        .dispatchPoseChanged(pose);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchPoseChanged", e);
+            }
+        }
+        mHeadPoseCallbacks.finishBroadcast();
+    }
 }