diff options
author | Vlad Popa <pvlad@google.com> | 2023-12-03 16:20:17 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-12-03 16:20:17 +0000 |
commit | 43b047027fa0ec6c7dfa4e678996acfb8f6dc7bd (patch) | |
tree | c31e7c2e0907719e9cd17e84b890c3d325a2df23 | |
parent | e689f58743c044a4b488818414e3977d7207ff5e (diff) | |
parent | 41f08b8d70260839a96d1ebb412eb8766b386b76 (diff) | |
download | base-43b047027fa0ec6c7dfa4e678996acfb8f6dc7bd.tar.gz |
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>
10 files changed, 694 insertions, 386 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 96cc1f892551..e8ed8b8a85d4 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9389,6 +9389,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> * 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 955bfcc902a4..467464093e10 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1208,6 +1208,9 @@ public class AudioSystem 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 @@ public class AudioSystem 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 cce515444c1f..da0689c6d177 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -687,6 +687,7 @@ public class SettingsBackupTest { 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 000000000000..a27daf65b9f9 --- /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 03dcc8d711d3..0682b7c47621 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.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; @@ -63,8 +64,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"; @@ -1462,6 +1466,9 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; // 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 dbe4fb8c8795..25a48f21468e 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 @@ 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("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 @@ public class AudioDeviceInventory { 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 @@ public class AudioDeviceInventory { 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 02648c4da76f..d01f694c6abc 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 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub mPlatformType = AudioSystem.getPlatformType(context); + mDeviceBroker = new AudioDeviceBroker(mContext, this); + mIsSingleVolume = AudioSystem.isSingleVolume(context); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -965,7 +966,7 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub } } + /*package*/ SettingsAdapter getSettings() { + return mSettings; + } + /////////////////////////////////////////////////////////////////////////// // Internal methods /////////////////////////////////////////////////////////////////////////// @@ -8129,10 +8132,6 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub } 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 5b26672c7de2..6972b1f9b82b 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.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.UUID; /** @@ -70,11 +71,12 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { } }; - 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 @@ public class SpatializerHelper { /** 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 @@ public class SpatializerHelper { */ 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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,34 +517,41 @@ public class SpatializerHelper { 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."; @@ -559,38 +561,57 @@ public class SpatializerHelper { // // 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())); } /** @@ -602,7 +623,7 @@ public class SpatializerHelper { // 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { } } - 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { 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 @@ public class SpatializerHelper { } } - 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 @@ public class SpatializerHelper { 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 dad9fe8648b2..b6b987c01a4a 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.bluetooth.BluetoothDevice; 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 @@ public class AudioDeviceBrokerTest { 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 @@ public class AudioDeviceBrokerTest { @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 @@ public class AudioDeviceBrokerTest { 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 b17c3a18d89e..bf38fd1669ad 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 @@ public class SpatializerHelperTest { @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); - } - - @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); + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + mSpyDeviceBroker = spy( + new AudioDeviceBroker( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + mMockAudioService)); + mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem, + mSpyDeviceBroker); } @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 @@ public class SpatializerHelperTest { 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 @@ public class SpatializerHelperTest { * 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); } |