Loading core/java/android/provider/Settings.java +7 −0 Original line number Diff line number Diff line Loading @@ -9925,6 +9925,13 @@ public final class Settings { */ 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> Loading packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -712,6 +712,7 @@ public class SettingsBackupTest { Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_TURNED_OFF_BY_POLICY, Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user Settings.Secure.BACKUP_AUTO_RESTORE, Settings.Secure.BACKUP_ENABLED, Settings.Secure.BACKUP_PROVISIONED, Loading services/core/java/com/android/server/audio/AdiDeviceState.java 0 → 100644 +204 −0 Original line number Diff line number Diff line /* * 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); } } services/core/java/com/android/server/audio/AudioDeviceBroker.java +102 −2 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import android.os.RemoteCallbackList; 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; Loading @@ -69,8 +70,11 @@ import java.util.UUID; 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"; Loading Loading @@ -1850,6 +1854,9 @@ import java.util.concurrent.atomic.AtomicBoolean; final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); } break; case MSG_PERSIST_AUDIO_DEVICE_SETTINGS: onPersistAudioDeviceSettings(); break; default: Log.wtf(TAG, "Invalid message " + msg.what); } Loading Loading @@ -1927,6 +1934,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; 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: Loading Loading @@ -2344,4 +2353,95 @@ import java.util.concurrent.atomic.AtomicBoolean; info.getId(), null /*mixerAttributes*/); } /** * 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(); } } services/core/java/com/android/server/audio/AudioDeviceInventory.java +85 −4 Original line number Diff line number Diff line Loading @@ -15,6 +15,8 @@ */ package com.android.server.audio; import static android.media.AudioSystem.isBluetoothDevice; import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; Loading Loading @@ -78,12 +80,51 @@ public class AudioDeviceInventory { 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("mDeviceCatalogLock") 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") Loading Loading @@ -341,6 +382,12 @@ public class AudioDeviceInventory { mAppliedPresetRolesInt.forEach((key, devices) -> { pw.println(" " + prefix + "preset: " + key.first + " role:" + key.second + " devices:" + devices); }); pw.println("\ndevices:\n"); synchronized (mDeviceInventoryLock) { for (AdiDeviceState device : mDeviceInventory) { pw.println("\t" + device + "\n"); } } } //------------------------------------------------------------ Loading Loading @@ -1198,7 +1245,7 @@ public class AudioDeviceInventory { AudioDeviceInfo device = Stream.of(connectedDevices) .filter(d -> d.getInternalType() == ada.getInternalType()) .filter(d -> (!AudioSystem.isBluetoothDevice(d.getInternalType()) .filter(d -> (!isBluetoothDevice(d.getInternalType()) || (d.getAddress().equals(ada.getAddress())))) .findFirst() .orElse(null); Loading Loading @@ -1619,7 +1666,7 @@ public class AudioDeviceInventory { } for (DeviceInfo di : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) { if (!isBluetoothDevice(di.mDeviceType)) { continue; } AudioDeviceAttributes ada = Loading Loading @@ -1733,7 +1780,7 @@ public class AudioDeviceInventory { } HashSet<String> processedAddresses = new HashSet<>(0); for (DeviceInfo di : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di.mDeviceType) if (!isBluetoothDevice(di.mDeviceType) || processedAddresses.contains(di.mDeviceAddress)) { continue; } Loading @@ -1743,7 +1790,7 @@ public class AudioDeviceInventory { + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles); } for (DeviceInfo di2 : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di2.mDeviceType) if (!isBluetoothDevice(di2.mDeviceType) || !di.mDeviceAddress.equals(di2.mDeviceAddress)) { continue; } Loading Loading @@ -2359,6 +2406,40 @@ public class AudioDeviceInventory { } } /*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 Loading Loading
core/java/android/provider/Settings.java +7 −0 Original line number Diff line number Diff line Loading @@ -9925,6 +9925,13 @@ public final class Settings { */ 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> Loading
packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -712,6 +712,7 @@ public class SettingsBackupTest { Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_TURNED_OFF_BY_POLICY, Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user Settings.Secure.BACKUP_AUTO_RESTORE, Settings.Secure.BACKUP_ENABLED, Settings.Secure.BACKUP_PROVISIONED, Loading
services/core/java/com/android/server/audio/AdiDeviceState.java 0 → 100644 +204 −0 Original line number Diff line number Diff line /* * 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); } }
services/core/java/com/android/server/audio/AudioDeviceBroker.java +102 −2 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import android.os.RemoteCallbackList; 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; Loading @@ -69,8 +70,11 @@ import java.util.UUID; 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"; Loading Loading @@ -1850,6 +1854,9 @@ import java.util.concurrent.atomic.AtomicBoolean; final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); } break; case MSG_PERSIST_AUDIO_DEVICE_SETTINGS: onPersistAudioDeviceSettings(); break; default: Log.wtf(TAG, "Invalid message " + msg.what); } Loading Loading @@ -1927,6 +1934,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; 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: Loading Loading @@ -2344,4 +2353,95 @@ import java.util.concurrent.atomic.AtomicBoolean; info.getId(), null /*mixerAttributes*/); } /** * 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(); } }
services/core/java/com/android/server/audio/AudioDeviceInventory.java +85 −4 Original line number Diff line number Diff line Loading @@ -15,6 +15,8 @@ */ package com.android.server.audio; import static android.media.AudioSystem.isBluetoothDevice; import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; Loading Loading @@ -78,12 +80,51 @@ public class AudioDeviceInventory { 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("mDeviceCatalogLock") 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") Loading Loading @@ -341,6 +382,12 @@ public class AudioDeviceInventory { mAppliedPresetRolesInt.forEach((key, devices) -> { pw.println(" " + prefix + "preset: " + key.first + " role:" + key.second + " devices:" + devices); }); pw.println("\ndevices:\n"); synchronized (mDeviceInventoryLock) { for (AdiDeviceState device : mDeviceInventory) { pw.println("\t" + device + "\n"); } } } //------------------------------------------------------------ Loading Loading @@ -1198,7 +1245,7 @@ public class AudioDeviceInventory { AudioDeviceInfo device = Stream.of(connectedDevices) .filter(d -> d.getInternalType() == ada.getInternalType()) .filter(d -> (!AudioSystem.isBluetoothDevice(d.getInternalType()) .filter(d -> (!isBluetoothDevice(d.getInternalType()) || (d.getAddress().equals(ada.getAddress())))) .findFirst() .orElse(null); Loading Loading @@ -1619,7 +1666,7 @@ public class AudioDeviceInventory { } for (DeviceInfo di : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) { if (!isBluetoothDevice(di.mDeviceType)) { continue; } AudioDeviceAttributes ada = Loading Loading @@ -1733,7 +1780,7 @@ public class AudioDeviceInventory { } HashSet<String> processedAddresses = new HashSet<>(0); for (DeviceInfo di : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di.mDeviceType) if (!isBluetoothDevice(di.mDeviceType) || processedAddresses.contains(di.mDeviceAddress)) { continue; } Loading @@ -1743,7 +1790,7 @@ public class AudioDeviceInventory { + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles); } for (DeviceInfo di2 : mConnectedDevices.values()) { if (!AudioSystem.isBluetoothDevice(di2.mDeviceType) if (!isBluetoothDevice(di2.mDeviceType) || !di.mDeviceAddress.equals(di2.mDeviceAddress)) { continue; } Loading Loading @@ -2359,6 +2406,40 @@ public class AudioDeviceInventory { } } /*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 Loading