Refactor the SADeviceState to AdiDeviceState am: 41f08b8d70

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/25240034

Change-Id: I30180521842a10bf3cc049aee17e9a8fdc341060
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 96cc1f8..e8ed8b8 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9389,6 +9389,13 @@
         public static final String SPATIAL_AUDIO_ENABLED = "spatial_audio_enabled";
 
         /**
+         * Internal collection of audio device inventory items
+         * The device item stored are {@link com.android.server.audio.AdiDeviceState}
+         * @hide
+         */
+        public static final String AUDIO_DEVICE_INVENTORY = "audio_device_inventory";
+
+        /**
          * Indicates whether notification display on the lock screen is enabled.
          * <p>
          * Type: int (0 for false, 1 for true)
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 955bfcc..4674640 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1208,6 +1208,9 @@
     public static final Set<Integer> DEVICE_IN_ALL_SCO_SET;
     /** @hide */
     public static final Set<Integer> DEVICE_IN_ALL_USB_SET;
+    /** @hide */
+    public static final Set<Integer> DEVICE_IN_ALL_BLE_SET;
+
     static {
         DEVICE_IN_ALL_SET = new HashSet<>();
         DEVICE_IN_ALL_SET.add(DEVICE_IN_COMMUNICATION);
@@ -1247,6 +1250,66 @@
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_ACCESSORY);
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_DEVICE);
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_HEADSET);
+
+        DEVICE_IN_ALL_BLE_SET = new HashSet<>();
+        DEVICE_IN_ALL_BLE_SET.add(DEVICE_IN_BLE_HEADSET);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothDevice(int deviceType) {
+        return isBluetoothA2dpOutDevice(deviceType)
+                || isBluetoothScoDevice(deviceType)
+                || isBluetoothLeDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothOutDevice(int deviceType) {
+        return isBluetoothA2dpOutDevice(deviceType)
+                || isBluetoothScoOutDevice(deviceType)
+                || isBluetoothLeOutDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothInDevice(int deviceType) {
+        return isBluetoothScoInDevice(deviceType)
+                || isBluetoothLeInDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothA2dpOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_A2DP_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_SCO_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoInDevice(int deviceType) {
+        return DEVICE_IN_ALL_SCO_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoDevice(int deviceType) {
+        return isBluetoothScoOutDevice(deviceType)
+                || isBluetoothScoInDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_BLE_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeInDevice(int deviceType) {
+        return DEVICE_IN_ALL_BLE_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeDevice(int deviceType) {
+        return isBluetoothLeOutDevice(deviceType)
+                || isBluetoothLeInDevice(deviceType);
     }
 
     /** @hide */
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index cce5154..da0689c 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -687,6 +687,7 @@
                  Settings.Secure.ASSIST_SCREENSHOT_ENABLED,
                  Settings.Secure.ASSIST_STRUCTURE_ENABLED,
                  Settings.Secure.ATTENTIVE_TIMEOUT,
+                 Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user
                  Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION,
                  Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT,
                  Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
new file mode 100644
index 0000000..a27daf6
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 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 com.android.server.audio;
+
+import static android.media.AudioSystem.DEVICE_NONE;
+import static android.media.AudioSystem.isBluetoothDevice;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Class representing all devices that were previously or are currently connected. Data is
+ * persisted in {@link android.provider.Settings.Secure}
+ */
+/*package*/ final class AdiDeviceState {
+    private static final String TAG = "AS.AdiDeviceState";
+
+    private static final String SETTING_FIELD_SEPARATOR = ",";
+
+    @AudioDeviceInfo.AudioDeviceType
+    private final int mDeviceType;
+
+    private final int mInternalDeviceType;
+    @NonNull
+    private final String mDeviceAddress;
+    private boolean mSAEnabled;
+    private boolean mHasHeadTracker = false;
+    private boolean mHeadTrackerEnabled;
+
+    /**
+     * Constructor
+     *
+     * @param deviceType external audio device type
+     * @param internalDeviceType if not set pass {@link DEVICE_NONE}, in this case the
+     *                           default conversion of the external type will be used
+     * @param address must be non-null for wireless devices
+     * @throws NullPointerException if a null address is passed for a wireless device
+     */
+    AdiDeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType,
+                        int internalDeviceType,
+                        @Nullable String address) {
+        mDeviceType = deviceType;
+        if (internalDeviceType != DEVICE_NONE) {
+            mInternalDeviceType = internalDeviceType;
+        } else {
+            mInternalDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType);
+
+        }
+        mDeviceAddress = isBluetoothDevice(mInternalDeviceType) ? Objects.requireNonNull(
+                address) : "";
+    }
+
+
+
+    @AudioDeviceInfo.AudioDeviceType
+    public int getDeviceType() {
+        return mDeviceType;
+    }
+
+    public int getInternalDeviceType() {
+        return mInternalDeviceType;
+    }
+
+    @NonNull
+    public String getDeviceAddress() {
+        return mDeviceAddress;
+    }
+
+    public void setSAEnabled(boolean sAEnabled) {
+        mSAEnabled = sAEnabled;
+    }
+
+    public boolean isSAEnabled() {
+        return mSAEnabled;
+    }
+
+    public void setHeadTrackerEnabled(boolean headTrackerEnabled) {
+        mHeadTrackerEnabled = headTrackerEnabled;
+    }
+
+    public boolean isHeadTrackerEnabled() {
+        return mHeadTrackerEnabled;
+    }
+
+    public void setHasHeadTracker(boolean hasHeadTracker) {
+        mHasHeadTracker = hasHeadTracker;
+    }
+
+
+    public boolean hasHeadTracker() {
+        return mHasHeadTracker;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        // type check and cast
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AdiDeviceState sads = (AdiDeviceState) obj;
+        return mDeviceType == sads.mDeviceType
+                && mInternalDeviceType == sads.mInternalDeviceType
+                && mDeviceAddress.equals(sads.mDeviceAddress)  // NonNull
+                && mSAEnabled == sads.mSAEnabled
+                && mHasHeadTracker == sads.mHasHeadTracker
+                && mHeadTrackerEnabled == sads.mHeadTrackerEnabled;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDeviceType, mInternalDeviceType, mDeviceAddress, mSAEnabled,
+                mHasHeadTracker, mHeadTrackerEnabled);
+    }
+
+    @Override
+    public String toString() {
+        return "type: " + mDeviceType + "internal type: " + mInternalDeviceType
+                + " addr: " + mDeviceAddress + " enabled: " + mSAEnabled
+                + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled;
+    }
+
+    public String toPersistableString() {
+        return (new StringBuilder().append(mDeviceType)
+                .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress)
+                .append(SETTING_FIELD_SEPARATOR).append(mSAEnabled ? "1" : "0")
+                .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0")
+                .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0")
+                .append(SETTING_FIELD_SEPARATOR).append(mInternalDeviceType)
+                .toString());
+    }
+
+    /**
+     * Gets the max size (including separators) when persisting the elements with
+     * {@link AdiDeviceState#toPersistableString()}.
+     */
+    public static int getPeristedMaxSize() {
+        return 36;  /* (mDeviceType)2 + (mDeviceAddresss)17 + (mInternalDeviceType)9 + (mSAEnabled)1
+                           + (mHasHeadTracker)1 + (mHasHeadTrackerEnabled)1
+                           + (SETTINGS_FIELD_SEPARATOR)5 */
+    }
+
+    @Nullable
+    public static AdiDeviceState fromPersistedString(@Nullable String persistedString) {
+        if (persistedString == null) {
+            return null;
+        }
+        if (persistedString.isEmpty()) {
+            return null;
+        }
+        String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR);
+        // we may have 5 fields for the legacy AdiDeviceState and 6 containing the internal
+        // device type
+        if (fields.length != 5 && fields.length != 6) {
+            // expecting all fields, fewer may mean corruption, ignore those settings
+            return null;
+        }
+        try {
+            final int deviceType = Integer.parseInt(fields[0]);
+            int internalDeviceType = -1;
+            if (fields.length == 6) {
+                internalDeviceType = Integer.parseInt(fields[5]);
+            }
+            final AdiDeviceState deviceState = new AdiDeviceState(deviceType,
+                    internalDeviceType, fields[1]);
+            deviceState.setHasHeadTracker(Integer.parseInt(fields[2]) == 1);
+            deviceState.setHasHeadTracker(Integer.parseInt(fields[3]) == 1);
+            deviceState.setHeadTrackerEnabled(Integer.parseInt(fields[4]) == 1);
+            return deviceState;
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "unable to parse setting for AdiDeviceState: " + persistedString, e);
+            return null;
+        }
+    }
+
+    public AudioDeviceAttributes getAudioDeviceAttributes() {
+        return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                mDeviceType, mDeviceAddress);
+    }
+
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 03dcc8d..0682b7c 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -46,6 +46,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.PrintWriterPrinter;
@@ -63,8 +64,11 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 
-/** @hide */
-/*package*/ final class AudioDeviceBroker {
+/**
+ * @hide
+ * (non final for mocking/spying)
+ */
+public class AudioDeviceBroker {
 
     private static final String TAG = "AS.AudioDeviceBroker";
 
@@ -1462,6 +1466,9 @@
                     final int capturePreset = msg.arg1;
                     mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
                 } break;
+                case MSG_PERSIST_AUDIO_DEVICE_SETTINGS:
+                    onPersistAudioDeviceSettings();
+                    break;
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -1534,6 +1541,8 @@
     // process set volume for Le Audio, obj is BleVolumeInfo
     private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
 
+    private static final int MSG_PERSIST_AUDIO_DEVICE_SETTINGS = 54;
+
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
             case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
@@ -1919,4 +1928,95 @@
             return mDeviceInventory.getDeviceSensorUuid(device);
         }
     }
+
+    /**
+     * post a message to persist the audio device settings.
+     * Message is delayed by 1s on purpose in case of successive changes in quick succession (at
+     * init time for instance)
+     * Note this method is made public to work around a Mockito bug where it needs to be public
+     * in order to be mocked by a test a the same package
+     * (see https://code.google.com/archive/p/mockito/issues/127)
+     */
+    public void persistAudioDeviceSettings() {
+        sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000);
+    }
+
+    void onPersistAudioDeviceSettings() {
+        final String deviceSettings = mDeviceInventory.getDeviceSettings();
+        Log.v(TAG, "saving audio device settings: " + deviceSettings);
+        final SettingsAdapter settings = mAudioService.getSettings();
+        boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(),
+                Settings.Secure.AUDIO_DEVICE_INVENTORY,
+                deviceSettings, UserHandle.USER_CURRENT);
+        if (!res) {
+            Log.e(TAG, "error saving audio device settings: " + deviceSettings);
+        }
+    }
+
+    void onReadAudioDeviceSettings() {
+        final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+        final ContentResolver contentResolver = mAudioService.getContentResolver();
+        String settings = settingsAdapter.getSecureStringForUser(contentResolver,
+                Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+        if (settings == null) {
+            Log.i(TAG, "reading spatial audio device settings from legacy key"
+                    + Settings.Secure.SPATIAL_AUDIO_ENABLED);
+            // legacy string format for key SPATIAL_AUDIO_ENABLED has the same order of fields like
+            // the strings for key AUDIO_DEVICE_INVENTORY. This will ensure to construct valid
+            // device settings when calling {@link #setDeviceSettings()}
+            settings = settingsAdapter.getSecureStringForUser(contentResolver,
+                    Settings.Secure.SPATIAL_AUDIO_ENABLED, UserHandle.USER_CURRENT);
+            if (settings == null) {
+                Log.i(TAG, "no spatial audio device settings stored with legacy key");
+            } else if (!settings.equals("")) {
+                // Delete old key value and update the new key
+                if (!settingsAdapter.putSecureStringForUser(contentResolver,
+                        Settings.Secure.SPATIAL_AUDIO_ENABLED,
+                        /*value=*/"",
+                        UserHandle.USER_CURRENT)) {
+                    Log.w(TAG, "cannot erase the legacy audio device settings with key "
+                            + Settings.Secure.SPATIAL_AUDIO_ENABLED);
+                }
+                if (!settingsAdapter.putSecureStringForUser(contentResolver,
+                        Settings.Secure.AUDIO_DEVICE_INVENTORY,
+                        settings,
+                        UserHandle.USER_CURRENT)) {
+                    Log.e(TAG, "error updating the new audio device settings with key "
+                            + Settings.Secure.AUDIO_DEVICE_INVENTORY);
+                }
+            }
+        }
+
+        if (settings != null && !settings.equals("")) {
+            setDeviceSettings(settings);
+        }
+    }
+
+    void setDeviceSettings(String settings) {
+        mDeviceInventory.setDeviceSettings(settings);
+    }
+
+    /** Test only method. */
+    String getDeviceSettings() {
+        return mDeviceInventory.getDeviceSettings();
+    }
+
+    List<AdiDeviceState> getImmutableDeviceInventory() {
+        return mDeviceInventory.getImmutableDeviceInventory();
+    }
+
+    void addDeviceStateToInventory(AdiDeviceState deviceState) {
+        mDeviceInventory.addDeviceStateToInventory(deviceState);
+    }
+
+    AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada,
+            int canonicalType) {
+        return mDeviceInventory.findDeviceStateForAudioDeviceAttributes(ada, canonicalType);
+    }
+
+    //------------------------------------------------
+    // for testing purposes only
+    void clearDeviceInventory() {
+        mDeviceInventory.clearDeviceInventory();
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index dbe4fb8..25a48f2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.audio;
 
+import static android.media.AudioSystem.isBluetoothDevice;
+
 import android.annotation.NonNull;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -61,12 +63,54 @@
 
     private static final String TAG = "AS.AudioDeviceInventory";
 
+    private static final String SETTING_DEVICE_SEPARATOR_CHAR = "|";
+    private static final String SETTING_DEVICE_SEPARATOR = "\\|";
+
     // lock to synchronize all access to mConnectedDevices and mApmConnectedDevices
     private final Object mDevicesLock = new Object();
 
     //Audio Analytics ids.
     private static final String mMetricsId = "audio.device.";
 
+    private final Object mDeviceInventoryLock = new Object();
+    @GuardedBy("mDeviceInventoryLock")
+    private final ArrayList<AdiDeviceState> mDeviceInventory = new ArrayList<>(0);
+
+
+    List<AdiDeviceState> getImmutableDeviceInventory() {
+        synchronized (mDeviceInventoryLock) {
+            return List.copyOf(mDeviceInventory);
+        }
+    }
+
+    void addDeviceStateToInventory(AdiDeviceState deviceState) {
+        synchronized (mDeviceInventoryLock) {
+            mDeviceInventory.add(deviceState);
+        }
+    }
+
+    AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada,
+            int canonicalDeviceType) {
+        final boolean isWireless = isBluetoothDevice(ada.getInternalType());
+
+        synchronized (mDeviceInventoryLock) {
+            for (AdiDeviceState deviceSetting : mDeviceInventory) {
+                if (deviceSetting.getDeviceType() == canonicalDeviceType
+                        && (!isWireless || ada.getAddress().equals(
+                        deviceSetting.getDeviceAddress()))) {
+                    return deviceSetting;
+                }
+            }
+        }
+        return null;
+    }
+
+    void clearDeviceInventory() {
+        synchronized (mDeviceInventoryLock) {
+            mDeviceInventory.clear();
+        }
+    }
+
     // List of connected devices
     // Key for map created from DeviceInfo.makeDeviceListKey()
     @GuardedBy("mDevicesLock")
@@ -262,6 +306,12 @@
         mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
             pw.println("  " + prefix + "capturePreset:" + capturePreset
                     + " devices:" + devices); });
+        pw.println("\ndevices:\n");
+        synchronized (mDeviceInventoryLock) {
+            for (AdiDeviceState device : mDeviceInventory) {
+                pw.println("\t" + device + "\n");
+            }
+        }
     }
 
     //------------------------------------------------------------
@@ -1506,6 +1556,41 @@
             return di.mSensorUuid;
         }
     }
+
+    /*package*/ String getDeviceSettings() {
+        int deviceCatalogSize = 0;
+        synchronized (mDeviceInventoryLock) {
+            deviceCatalogSize = mDeviceInventory.size();
+        }
+        final StringBuilder settingsBuilder = new StringBuilder(
+                deviceCatalogSize * AdiDeviceState.getPeristedMaxSize());
+
+        synchronized (mDeviceInventoryLock) {
+            for (int i = 0; i < mDeviceInventory.size(); i++) {
+                settingsBuilder.append(mDeviceInventory.get(i).toPersistableString());
+                if (i != mDeviceInventory.size() - 1) {
+                    settingsBuilder.append(SETTING_DEVICE_SEPARATOR_CHAR);
+                }
+            }
+        }
+        return settingsBuilder.toString();
+    }
+
+    /*package*/ void setDeviceSettings(String settings) {
+        clearDeviceInventory();
+        String[] devSettings = TextUtils.split(Objects.requireNonNull(settings),
+                SETTING_DEVICE_SEPARATOR);
+        // small list, not worth overhead of Arrays.stream(devSettings)
+        for (String setting : devSettings) {
+            AdiDeviceState devState = AdiDeviceState.fromPersistedString(setting);
+            // Note if the device is not compatible with spatialization mode or the device
+            // type is not canonical, it will be ignored in {@link SpatializerHelper}.
+            if (devState != null) {
+                addDeviceStateToInventory(devState);
+            }
+        }
+    }
+
     //----------------------------------------------------------
     // For tests only
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 02648c4..d01f694 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -341,7 +341,6 @@
     private static final int MSG_DISPATCH_AUDIO_MODE = 40;
     private static final int MSG_ROUTING_UPDATED = 41;
     private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
-    private static final int MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS = 43;
     private static final int MSG_ADD_ASSISTANT_SERVICE_UID = 44;
     private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45;
     private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46;
@@ -953,6 +952,8 @@
 
         mPlatformType = AudioSystem.getPlatformType(context);
 
+        mDeviceBroker = new AudioDeviceBroker(mContext, this);
+
         mIsSingleVolume = AudioSystem.isSingleVolume(context);
 
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -965,7 +966,7 @@
 
         mSfxHelper = new SoundEffectsHelper(mContext);
 
-        mSpatializerHelper = new SpatializerHelper(this, mAudioSystem);
+        mSpatializerHelper = new SpatializerHelper(this, mAudioSystem, mDeviceBroker);
 
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
         mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator();
@@ -1101,8 +1102,6 @@
         mUseFixedVolume = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useFixedVolume);
 
-        mDeviceBroker = new AudioDeviceBroker(mContext, this);
-
         mRecordMonitor = new RecordingActivityMonitor(mContext);
         mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true);
 
@@ -5925,6 +5924,10 @@
         }
     }
 
+    /*package*/ SettingsAdapter getSettings() {
+        return mSettings;
+    }
+
     ///////////////////////////////////////////////////////////////////////////
     // Internal methods
     ///////////////////////////////////////////////////////////////////////////
@@ -8129,10 +8132,6 @@
                     mSpatializerHelper.onInitSensors();
                     break;
 
-                case MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS:
-                    onPersistSpatialAudioDeviceSettings();
-                    break;
-
                 case MSG_CHECK_MUSIC_ACTIVE:
                     onCheckMusicActive((String) msg.obj);
                     break;
@@ -8142,6 +8141,7 @@
                     onConfigureSafeVolume((msg.what == MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED),
                             (String) msg.obj);
                     break;
+
                 case MSG_PERSIST_SAFE_VOLUME_STATE:
                     onPersistSafeVolumeState(msg.arg1);
                     break;
@@ -9102,44 +9102,11 @@
     }
 
     void onInitSpatializer() {
-        final String settings = mSettings.getSecureStringForUser(mContentResolver,
-                Settings.Secure.SPATIAL_AUDIO_ENABLED, UserHandle.USER_CURRENT);
-        if (settings == null) {
-            Log.e(TAG, "error reading spatial audio device settings");
-        } else {
-            Log.v(TAG, "restoring spatial audio device settings: " + settings);
-            mSpatializerHelper.setSADeviceSettings(settings);
-        }
+        mDeviceBroker.onReadAudioDeviceSettings();
         mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
         mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect);
     }
 
-    /**
-     * post a message to persist the spatial audio device settings.
-     * Message is delayed by 1s on purpose in case of successive changes in quick succession (at
-     * init time for instance)
-     * Note this method is made public to work around a Mockito bug where it needs to be public
-     * in order to be mocked by a test a the same package
-     * (see https://code.google.com/archive/p/mockito/issues/127)
-     */
-    public void persistSpatialAudioDeviceSettings() {
-        sendMsg(mAudioHandler,
-                MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS,
-                SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, TAG,
-                /*delay*/ 1000);
-    }
-
-    void onPersistSpatialAudioDeviceSettings() {
-        final String settings = mSpatializerHelper.getSADeviceSettings();
-        Log.v(TAG, "saving spatial audio device settings: " + settings);
-        boolean res = mSettings.putSecureStringForUser(mContentResolver,
-                Settings.Secure.SPATIAL_AUDIO_ENABLED,
-                settings, UserHandle.USER_CURRENT);
-        if (!res) {
-            Log.e(TAG, "error saving spatial audio device settings: " + settings);
-        }
-    }
-
     //==========================================================================================
     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 5b26672..6972b1f 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.server.audio;
 
+import static android.media.AudioSystem.isBluetoothDevice;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -50,7 +52,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
-import java.util.Objects;
 import java.util.UUID;
 
 /**
@@ -70,11 +71,12 @@
 
     private final @NonNull AudioSystemAdapter mASA;
     private final @NonNull AudioService mAudioService;
+    private final @NonNull AudioDeviceBroker mDeviceBroker;
     private @Nullable SensorManager mSensorManager;
 
     //------------------------------------------------------------
 
-    private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) {
+    /*package*/ static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) {
         {
             append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL);
             append(AudioDeviceInfo.TYPE_WIRED_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL);
@@ -96,17 +98,6 @@
         }
     };
 
-    private static final int[] WIRELESS_TYPES = { AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
-            AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
-            AudioDeviceInfo.TYPE_BLE_HEADSET,
-            AudioDeviceInfo.TYPE_BLE_SPEAKER,
-            AudioDeviceInfo.TYPE_BLE_BROADCAST
-    };
-
-    private static final int[] WIRELESS_SPEAKER_TYPES = {
-            AudioDeviceInfo.TYPE_BLE_SPEAKER,
-    };
-
     // Spatializer state machine
     private static final int STATE_UNINITIALIZED = 0;
     private static final int STATE_NOT_SUPPORTED = 1;
@@ -120,6 +111,7 @@
     /** 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 boolean mTransauralSupported = false;
     private boolean mBinauralSupported = false;
     private boolean mIsHeadTrackingSupported = false;
@@ -162,17 +154,13 @@
      */
     private final ArrayList<Integer> mSACapableDeviceTypes = new ArrayList<>(0);
 
-    /**
-     * List of devices where Spatial Audio is possible. Each device can be enabled or disabled
-     * (== user choice to use or not)
-     */
-    private final ArrayList<SADeviceState> mSADevices = new ArrayList<>(0);
-
     //------------------------------------------------------
     // initialization
-    SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa) {
+    SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa,
+            @NonNull AudioDeviceBroker deviceBroker) {
         mAudioService = mother;
         mASA = asa;
+        mDeviceBroker = deviceBroker;
     }
 
     synchronized void init(boolean effectExpected) {
@@ -278,6 +266,14 @@
                     mSACapableDeviceTypes.add(SPAT_MODE_FOR_DEVICE_TYPE.keyAt(i));
                 }
             }
+
+            // Log the saved device states that are compatible with SA
+            for (AdiDeviceState deviceState : mDeviceBroker.getImmutableDeviceInventory()) {
+                if (isSADevice(deviceState)) {
+                    logDeviceState(deviceState, "setSADeviceSettings");
+                }
+            }
+
             // for both transaural / binaural, we are not forcing enablement as the init() method
             // could have been called another time after boot in case of audioserver restart
             if (mTransauralSupported) {
@@ -321,7 +317,7 @@
         mState = STATE_UNINITIALIZED;
         mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
         mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
-        init(true);
+        init(/*effectExpected=*/true);
         setSpatializerEnabledInt(featureEnabled);
     }
 
@@ -353,7 +349,7 @@
                 DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES);
 
         // is media routed to a new device?
-        if (isWireless(ROUTING_DEVICES[0].getType())) {
+        if (isBluetoothDevice(ROUTING_DEVICES[0].getInternalType())) {
             addWirelessDeviceIfNew(ROUTING_DEVICES[0]);
         }
 
@@ -497,10 +493,9 @@
     synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
         // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices
         ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>();
-        for (SADeviceState dev : mSADevices) {
-            if (dev.mEnabled) {
-                compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
-                        dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress));
+        for (AdiDeviceState deviceState : mDeviceBroker.getImmutableDeviceInventory()) {
+            if (deviceState.isSAEnabled() && isSADevice(deviceState)) {
+                compatList.add(deviceState.getAudioDeviceAttributes());
             }
         }
         return compatList;
@@ -522,36 +517,43 @@
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
             boolean forceEnable) {
         loglogi("addCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-        boolean isInList = false;
-        SADeviceState deviceUpdated = null; // non-null on update.
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
-                isInList = true;
-                if (forceEnable) {
-                    deviceState.mEnabled = true;
-                    deviceUpdated = deviceState;
-                }
-                break;
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        initSAState(deviceState);
+        AdiDeviceState updatedDevice = null; // non-null on update.
+        if (deviceState != null) {
+            if (forceEnable && !deviceState.isSAEnabled()) {
+                updatedDevice = deviceState;
+                updatedDevice.setSAEnabled(true);
             }
+        } else {
+            // When adding, force the device type to be a canonical one.
+            final int canonicalDeviceType = getCanonicalDeviceType(ada.getType(),
+                    ada.getInternalType());
+            if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) {
+                Log.e(TAG, "addCompatibleAudioDevice with incompatible AudioDeviceAttributes "
+                        + ada);
+                return;
+            }
+            updatedDevice = new AdiDeviceState(canonicalDeviceType, ada.getInternalType(),
+                    ada.getAddress());
+            initSAState(updatedDevice);
+            mDeviceBroker.addDeviceStateToInventory(updatedDevice);
         }
-        if (!isInList) {
-            final SADeviceState dev = new SADeviceState(deviceType,
-                    wireless ? ada.getAddress() : "");
-            dev.mEnabled = true;
-            mSADevices.add(dev);
-            deviceUpdated = dev;
-        }
-        if (deviceUpdated != null) {
+        if (updatedDevice != null) {
             onRoutingUpdated();
-            mAudioService.persistSpatialAudioDeviceSettings();
-            logDeviceState(deviceUpdated, "addCompatibleAudioDevice");
+            mDeviceBroker.persistAudioDeviceSettings();
+            logDeviceState(updatedDevice, "addCompatibleAudioDevice");
         }
     }
 
+    private void initSAState(AdiDeviceState device) {
+        if (device == null) {
+            return;
+        }
+        device.setSAEnabled(true);
+        device.setHeadTrackerEnabled(true);
+    }
+
     private static final String METRICS_DEVICE_PREFIX = "audio.spatializer.device.";
 
     // Device logging is accomplished in the Java Audio Service level.
@@ -559,41 +561,60 @@
     //
     // There may be different devices with the same device type (aliasing).
     // We always send the full device state info on each change.
-    private void logDeviceState(SADeviceState deviceState, String event) {
-        final String deviceName = AudioSystem.getDeviceName(deviceState.mDeviceType);
+    static void logDeviceState(AdiDeviceState deviceState, String event) {
+        final String deviceName = AudioSystem.getDeviceName(deviceState.getInternalDeviceType());
         new MediaMetrics.Item(METRICS_DEVICE_PREFIX + deviceName)
-            .set(MediaMetrics.Property.ADDRESS, deviceState.mDeviceAddress)
-            .set(MediaMetrics.Property.ENABLED, deviceState.mEnabled ? "true" : "false")
-            .set(MediaMetrics.Property.EVENT, TextUtils.emptyIfNull(event))
-            .set(MediaMetrics.Property.HAS_HEAD_TRACKER,
-                    deviceState.mHasHeadTracker ? "true" : "false") // this may be updated later.
-            .set(MediaMetrics.Property.HEAD_TRACKER_ENABLED,
-                    deviceState.mHeadTrackerEnabled ? "true" : "false")
-            .record();
+                .set(MediaMetrics.Property.ADDRESS, deviceState.getDeviceAddress())
+                .set(MediaMetrics.Property.ENABLED, deviceState.isSAEnabled() ? "true" : "false")
+                .set(MediaMetrics.Property.EVENT, TextUtils.emptyIfNull(event))
+                .set(MediaMetrics.Property.HAS_HEAD_TRACKER,
+                        deviceState.hasHeadTracker() ? "true"
+                                : "false") // this may be updated later.
+                .set(MediaMetrics.Property.HEAD_TRACKER_ENABLED,
+                        deviceState.isHeadTrackerEnabled() ? "true" : "false")
+                .record();
     }
 
     synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
         loglogi("removeCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-        SADeviceState deviceUpdated = null; // non-null on update.
 
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
-                deviceState.mEnabled = false;
-                deviceUpdated = deviceState;
-                break;
-            }
-        }
-        if (deviceUpdated != null) {
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState != null && deviceState.isSAEnabled()) {
+            deviceState.setSAEnabled(false);
             onRoutingUpdated();
-            mAudioService.persistSpatialAudioDeviceSettings();
-            logDeviceState(deviceUpdated, "removeCompatibleAudioDevice");
+            mDeviceBroker.persistAudioDeviceSettings();
+            logDeviceState(deviceState, "removeCompatibleAudioDevice");
         }
     }
 
     /**
+     * Returns a possibly aliased device type which is used
+     * for spatial audio settings (or TYPE_UNKNOWN  if it doesn't exist).
+     */
+    @AudioDeviceInfo.AudioDeviceType
+    private static int getCanonicalDeviceType(int deviceType, int internalDeviceType) {
+        if (isBluetoothDevice(internalDeviceType)) return deviceType;
+
+        final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE);
+        if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) {
+            return AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+        } else if (spatMode == SpatializationMode.SPATIALIZER_BINAURAL) {
+            return AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
+        }
+        return AudioDeviceInfo.TYPE_UNKNOWN;
+    }
+
+    /**
+     * Returns the Spatial Audio device state for an audio device attributes
+     * or null if it does not exist.
+     */
+    @Nullable
+    private AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada) {
+        return mDeviceBroker.findDeviceStateForAudioDeviceAttributes(ada,
+                getCanonicalDeviceType(ada.getType(), ada.getInternalType()));
+    }
+
+    /**
      * Return if Spatial Audio is enabled and available for the given device
      * @param ada
      * @return a pair of boolean, 1/ enabled? 2/ available?
@@ -602,7 +623,7 @@
         // if not a wireless device, this value will be overwritten to map the type
         // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES
         @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
+        final boolean wireless = isBluetoothDevice(ada.getInternalType());
 
         // if not a wireless device: find if media device is in the speaker, wired headphones
         if (!wireless) {
@@ -625,7 +646,8 @@
                 deviceType = AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
             }
         } else { // wireless device
-            if (isWirelessSpeaker(deviceType) && !mTransauralSupported) {
+            if (ada.getInternalType() == AudioSystem.DEVICE_OUT_BLE_SPEAKER
+                    && !mTransauralSupported) {
                 Log.i(TAG, "Device incompatible with Spatial Audio (no transaural) dev:"
                         + ada);
                 return new Pair<>(false, false);
@@ -637,34 +659,35 @@
             }
         }
 
-        boolean enabled = false;
-        boolean available = false;
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                available = true;
-                enabled = deviceState.mEnabled;
-                break;
-            }
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState == null) {
+            // no matching device state?
+            Log.i(TAG, "no spatialization device state found for Spatial Audio device:" + ada);
+            return new Pair<>(false, false);
         }
-        return new Pair<>(enabled, available);
+        // found the matching device state.
+        return new Pair<>(deviceState.isSAEnabled(), true);
     }
 
     private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) {
-        boolean knownDevice = false;
-        for (SADeviceState deviceState : mSADevices) {
-            // wireless device so always check address
-            if (ada.getType() == deviceState.mDeviceType
-                    && ada.getAddress().equals(deviceState.mDeviceAddress)) {
-                knownDevice = true;
-                break;
-            }
+        if (!isDeviceCompatibleWithSpatializationModes(ada)) {
+            return;
         }
-        if (!knownDevice) {
-            final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress());
-            mSADevices.add(deviceState);
-            mAudioService.persistSpatialAudioDeviceSettings();
+        if (findDeviceStateForAudioDeviceAttributes(ada) == null) {
+            // wireless device types should be canonical, but we translate to be sure.
+            final int canonicalDeviceType = getCanonicalDeviceType(ada.getType(),
+                    ada.getInternalType());
+            if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) {
+                Log.e(TAG, "addWirelessDeviceIfNew with incompatible AudioDeviceAttributes "
+                        + ada);
+                return;
+            }
+            final AdiDeviceState deviceState =
+                    new AdiDeviceState(canonicalDeviceType, ada.getInternalType(),
+                            ada.getAddress());
+            initSAState(deviceState);
+            mDeviceBroker.addDeviceStateToInventory(deviceState);
+            mDeviceBroker.persistAudioDeviceSettings();
             logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
         }
     }
@@ -705,16 +728,7 @@
             return false;
         }
 
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                return true;
-            }
-        }
-        return false;
+        return findDeviceStateForAudioDeviceAttributes(ada) != null;
     }
 
     private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
@@ -729,6 +743,26 @@
         return false;
     }
 
+    private boolean isSADevice(AdiDeviceState deviceState) {
+        return deviceState.getDeviceType() == getCanonicalDeviceType(deviceState.getDeviceType(),
+                deviceState.getInternalDeviceType()) && isDeviceCompatibleWithSpatializationModes(
+                deviceState.getAudioDeviceAttributes());
+    }
+
+    private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) {
+        // modeForDevice will be neither transaural or binaural for devices that do not support
+        // spatial audio. For instance mono devices like earpiece, speaker safe or sco must
+        // not be included.
+        final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(),
+            /*default when type not found*/ -1);
+        if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported)
+                || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL
+                    && mTransauralSupported)) {
+            return true;
+        }
+        return false;
+    }
+
     synchronized void setFeatureEnabled(boolean enabled) {
         loglogi("setFeatureEnabled(" + enabled + ") was featureEnabled:" + mFeatureEnabled);
         if (mFeatureEnabled == enabled) {
@@ -1089,27 +1123,20 @@
             Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled
                     + " for " + ada);
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                if (!deviceState.mHasHeadTracker) {
-                    Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
-                            + " device:" + ada + " on a device without headtracker");
-                    return;
-                }
-                Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
-                deviceState.mHeadTrackerEnabled = enabled;
-                mAudioService.persistSpatialAudioDeviceSettings();
-                logDeviceState(deviceState, "setHeadTrackerEnabled");
-                break;
-            }
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState == null) return;
+        if (!deviceState.hasHeadTracker()) {
+            Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
+                    + " device:" + ada + " on a device without headtracker");
+            return;
         }
+        Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
+        deviceState.setHeadTrackerEnabled(enabled);
+        mDeviceBroker.persistAudioDeviceSettings();
+        logDeviceState(deviceState, "setHeadTrackerEnabled");
+
         // check current routing to see if it affects the headtracking mode
-        if (ROUTING_DEVICES[0].getType() == deviceType
+        if (ROUTING_DEVICES[0].getType() == ada.getType()
                 && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
             setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled
                     : Spatializer.HEAD_TRACKING_MODE_DISABLED);
@@ -1121,17 +1148,8 @@
             Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                return deviceState.mHasHeadTracker;
-            }
-        }
-        return false;
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        return deviceState != null && deviceState.hasHeadTracker();
     }
 
     /**
@@ -1144,20 +1162,14 @@
             Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                if (!deviceState.mHasHeadTracker) {
-                    deviceState.mHasHeadTracker = true;
-                    mAudioService.persistSpatialAudioDeviceSettings();
-                    logDeviceState(deviceState, "setHasHeadTracker");
-                }
-                return deviceState.mHeadTrackerEnabled;
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState != null) {
+            if (!deviceState.hasHeadTracker()) {
+                deviceState.setHasHeadTracker(true);
+                mDeviceBroker.persistAudioDeviceSettings();
+                logDeviceState(deviceState, "setHasHeadTracker");
             }
+            return deviceState.isHeadTrackerEnabled();
         }
         Log.e(TAG, "setHasHeadTracker: device not found for:" + ada);
         return false;
@@ -1168,20 +1180,9 @@
             Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
-                if (!deviceState.mHasHeadTracker) {
-                    return false;
-                }
-                return deviceState.mHeadTrackerEnabled;
-            }
-        }
-        return false;
+        final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        return deviceState != null
+                && deviceState.hasHeadTracker() && deviceState.isHeadTrackerEnabled();
     }
 
     synchronized boolean isHeadTrackerAvailable() {
@@ -1513,117 +1514,6 @@
         pw.println("\tsupports binaural:" + mBinauralSupported + " / transaural:"
                 + mTransauralSupported);
         pw.println("\tmSpatOutput:" + mSpatOutput);
-        pw.println("\tdevices:");
-        for (SADeviceState device : mSADevices) {
-            pw.println("\t\t" + device);
-        }
-    }
-
-    /*package*/ static final class SADeviceState {
-        final @AudioDeviceInfo.AudioDeviceType int mDeviceType;
-        final @NonNull String mDeviceAddress;
-        boolean mEnabled = true;               // by default, SA is enabled on any device
-        boolean mHasHeadTracker = false;
-        boolean mHeadTrackerEnabled = true;    // by default, if head tracker is present, use it
-        static final String SETTING_FIELD_SEPARATOR = ",";
-        static final String SETTING_DEVICE_SEPARATOR_CHAR = "|";
-        static final String SETTING_DEVICE_SEPARATOR = "\\|";
-
-        SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) {
-            mDeviceType = deviceType;
-            mDeviceAddress = Objects.requireNonNull(address);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (obj == null) {
-                return false;
-            }
-            // type check and cast
-            if (getClass() != obj.getClass()) {
-                return false;
-            }
-            final SADeviceState sads = (SADeviceState) obj;
-            return mDeviceType == sads.mDeviceType
-                    && mDeviceAddress.equals(sads.mDeviceAddress)
-                    && mEnabled == sads.mEnabled
-                    && mHasHeadTracker == sads.mHasHeadTracker
-                    && mHeadTrackerEnabled == sads.mHeadTrackerEnabled;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mDeviceType, mDeviceAddress, mEnabled, mHasHeadTracker,
-                    mHeadTrackerEnabled);
-        }
-
-        @Override
-        public String toString() {
-            return "type:" + mDeviceType + " addr:" + mDeviceAddress + " enabled:" + mEnabled
-                    + " HT:" + mHasHeadTracker + " HTenabled:" + mHeadTrackerEnabled;
-        }
-
-        String toPersistableString() {
-            return (new StringBuilder().append(mDeviceType)
-                    .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress)
-                    .append(SETTING_FIELD_SEPARATOR).append(mEnabled ? "1" : "0")
-                    .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0")
-                    .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0")
-                    .toString());
-        }
-
-        static @Nullable SADeviceState fromPersistedString(@Nullable String persistedString) {
-            if (persistedString == null) {
-                return null;
-            }
-            if (persistedString.isEmpty()) {
-                return null;
-            }
-            String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR);
-            if (fields.length != 5) {
-                // expecting all fields, fewer may mean corruption, ignore those settings
-                return null;
-            }
-            try {
-                final int deviceType = Integer.parseInt(fields[0]);
-                final SADeviceState deviceState = new SADeviceState(deviceType, fields[1]);
-                deviceState.mEnabled = Integer.parseInt(fields[2]) == 1;
-                deviceState.mHasHeadTracker = Integer.parseInt(fields[3]) == 1;
-                deviceState.mHeadTrackerEnabled = Integer.parseInt(fields[4]) == 1;
-                return deviceState;
-            } catch (NumberFormatException e) {
-                Log.e(TAG, "unable to parse setting for SADeviceState: " + persistedString, e);
-                return null;
-            }
-        }
-    }
-
-    /*package*/ synchronized String getSADeviceSettings() {
-        // expected max size of each String for each SADeviceState is 25 (accounting for separator)
-        final StringBuilder settingsBuilder = new StringBuilder(mSADevices.size() * 25);
-        for (int i = 0; i < mSADevices.size(); i++) {
-            settingsBuilder.append(mSADevices.get(i).toPersistableString());
-            if (i != mSADevices.size() - 1) {
-                settingsBuilder.append(SADeviceState.SETTING_DEVICE_SEPARATOR_CHAR);
-            }
-        }
-        return settingsBuilder.toString();
-    }
-
-    /*package*/ synchronized void setSADeviceSettings(@NonNull String persistedSettings) {
-        String[] devSettings = TextUtils.split(Objects.requireNonNull(persistedSettings),
-                SADeviceState.SETTING_DEVICE_SEPARATOR);
-        // small list, not worth overhead of Arrays.stream(devSettings)
-        for (String setting : devSettings) {
-            SADeviceState devState = SADeviceState.fromPersistedString(setting);
-            if (devState != null) {
-                mSADevices.add(devState);
-                logDeviceState(devState, "setSADeviceSettings");
-            }
-        }
     }
 
     private static String spatStateString(int state) {
@@ -1645,24 +1535,6 @@
         }
     }
 
-    private static boolean isWireless(@AudioDeviceInfo.AudioDeviceType int deviceType) {
-        for (int type : WIRELESS_TYPES) {
-            if (type == deviceType) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static boolean isWirelessSpeaker(@AudioDeviceInfo.AudioDeviceType int deviceType) {
-        for (int type : WIRELESS_SPEAKER_TYPES) {
-            if (type == deviceType) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private int getHeadSensorHandleUpdateTracker() {
         int headHandle = -1;
         UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(ROUTING_DEVICES[0]);
@@ -1707,11 +1579,4 @@
         AudioService.sSpatialLogger.loglog(msg, AudioEventLogger.Event.ALOGE, TAG);
         return msg;
     }
-
-    //------------------------------------------------
-    // for testing purposes only
-
-    /*package*/ void clearSADevices() {
-        mSADevices.clear();
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index dad9fe8..b6b987c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -29,13 +29,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.BluetoothProfileConnectionInfo;
 import android.util.Log;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
@@ -54,7 +55,6 @@
     private static final String TAG = "AudioDeviceBrokerTest";
     private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
 
-    private Context mContext;
     // the actual class under test
     private AudioDeviceBroker mAudioDeviceBroker;
 
@@ -67,13 +67,13 @@
 
     @Before
     public void setUp() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
 
         mMockAudioService = mock(AudioService.class);
         mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
         mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
         mSpySystemServer = spy(new NoOpSystemServerAdapter());
-        mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory,
+        mAudioDeviceBroker = new AudioDeviceBroker(context, mMockAudioService, mSpyDevInventory,
                 mSpySystemServer);
         mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
 
@@ -197,6 +197,37 @@
                 any(Intent.class));
     }
 
+    /**
+     * Test that constructing an AdiDeviceState instance requires a non-null address for a
+     * wireless type, but can take null for a non-wireless type;
+     * @throws Exception
+     */
+    @Test
+    public void testAdiDeviceStateNullAddressCtor() throws Exception {
+        try {
+            new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+                    AudioManager.DEVICE_OUT_SPEAKER, null);
+            new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+                    AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null);
+            Assert.fail();
+        } catch (NullPointerException e) { }
+    }
+
+    @Test
+    public void testAdiDeviceStateStringSerialization() throws Exception {
+        Log.i(TAG, "starting testAdiDeviceStateStringSerialization");
+        final AdiDeviceState devState = new AdiDeviceState(
+                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla");
+        devState.setHasHeadTracker(false);
+        devState.setHeadTrackerEnabled(false);
+        devState.setSAEnabled(true);
+        final String persistString = devState.toPersistableString();
+        final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString);
+        Log.i(TAG, "original:" + devState);
+        Log.i(TAG, "result  :" + result);
+        Assert.assertEquals(devState, result);
+    }
+
     private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
             boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception {
         when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
index b17c3a1..bf38fd16 100644
--- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -15,18 +15,19 @@
  */
 package com.android.server.audio;
 
-import com.android.server.audio.SpatializerHelper.SADeviceState;
-
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 
 import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
 import android.media.AudioSystem;
 import android.util.Log;
 
 import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Assert;
@@ -49,41 +50,23 @@
 
     @Mock private AudioService mMockAudioService;
     @Spy private AudioSystemAdapter mSpyAudioSystem;
+    @Spy private AudioDeviceBroker mSpyDeviceBroker;
 
     @Before
     public void setUp() throws Exception {
         mMockAudioService = mock(AudioService.class);
+
         mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
-
-        mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem);
+        mSpyDeviceBroker = spy(
+                new AudioDeviceBroker(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        mMockAudioService));
+        mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem,
+                mSpyDeviceBroker);
     }
 
     @Test
-    public void testSADeviceStateNullAddressCtor() throws Exception {
-        try {
-            SADeviceState devState = new SADeviceState(
-                    AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
-            Assert.fail();
-        } catch (NullPointerException e) { }
-    }
-
-    @Test
-    public void testSADeviceStateStringSerialization() throws Exception {
-        Log.i(TAG, "starting testSADeviceStateStringSerialization");
-        final SADeviceState devState = new SADeviceState(
-                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla");
-        devState.mHasHeadTracker = false;
-        devState.mHeadTrackerEnabled = false;
-        devState.mEnabled = true;
-        final String persistString = devState.toPersistableString();
-        final SADeviceState result = SADeviceState.fromPersistedString(persistString);
-        Log.i(TAG, "original:" + devState);
-        Log.i(TAG, "result  :" + result);
-        Assert.assertEquals(devState, result);
-    }
-
-    @Test
-    public void testSADeviceSettings() throws Exception {
+    public void testAdiDeviceStateSettings() throws Exception {
         Log.i(TAG, "starting testSADeviceSettings");
         final AudioDeviceAttributes dev1 =
                 new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, "");
@@ -92,7 +75,7 @@
         final AudioDeviceAttributes dev3 =
                 new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop");
 
-        doNothing().when(mMockAudioService).persistSpatialAudioDeviceSettings();
+        doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings();
 
         // test with single device
         mSpatHelper.addCompatibleAudioDevice(dev1);
@@ -126,11 +109,11 @@
      * the original one.
      */
     private void checkAddSettings() throws Exception {
-        String settings = mSpatHelper.getSADeviceSettings();
+        String settings = mSpyDeviceBroker.getDeviceSettings();
         Log.i(TAG, "device settings: " + settings);
-        mSpatHelper.clearSADevices();
-        mSpatHelper.setSADeviceSettings(settings);
-        String settingsRestored = mSpatHelper.getSADeviceSettings();
+        mSpyDeviceBroker.clearDeviceInventory();
+        mSpyDeviceBroker.setDeviceSettings(settings);
+        String settingsRestored = mSpyDeviceBroker.getDeviceSettings();
         Log.i(TAG, "device settingsRestored: " + settingsRestored);
         Assert.assertEquals(settings, settingsRestored);
     }