Loading services/core/java/com/android/server/audio/AudioService.java +45 −4 Original line number Diff line number Diff line Loading @@ -341,8 +341,7 @@ 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; // commented out for now, will be reused for other SA persisting //private static final int MSG_PERSIST_SPATIAL_AUDIO_ENABLED = 43; 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; Loading Loading @@ -8122,8 +8121,7 @@ public class AudioService extends IAudioService.Stub break; case MSG_INIT_SPATIALIZER: mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect); mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); onInitSpatializer(); mAudioEventWakeLock.release(); break; Loading @@ -8131,6 +8129,10 @@ 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; Loading Loading @@ -9099,6 +9101,45 @@ public class AudioService extends IAudioService.Stub /*arg1*/ 0, /*arg2*/ 0, TAG, /*delay*/ 0); } 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); } 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) || Loading services/core/java/com/android/server/audio/SpatializerHelper.java +111 −10 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.media.Spatializer; import android.media.SpatializerHeadTrackingMode; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseIntArray; Loading @@ -48,6 +49,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; /** Loading Loading @@ -275,18 +277,17 @@ public class SpatializerHelper { // 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) { // TODO deal with persisted values // not force-enabling as this device might already be in the device list addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), false /*forceEnable*/); } if (mBinauralSupported) { // TODO deal with persisted values // not force-enabling as this device might already be in the device list addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), false /*forceEnable*/); } // TODO read persisted states } catch (RemoteException e) { resetCapabilities(); } finally { Loading Loading @@ -533,11 +534,12 @@ public class SpatializerHelper { } if (!isInList) { final SADeviceState dev = new SADeviceState(deviceType, wireless ? ada.getAddress() : null); wireless ? ada.getAddress() : ""); dev.mEnabled = true; mSADevices.add(dev); } onRoutingUpdated(); mAudioService.persistSpatialAudioDeviceSettings(); } synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { Loading @@ -553,6 +555,7 @@ public class SpatializerHelper { } } onRoutingUpdated(); mAudioService.persistSpatialAudioDeviceSettings(); } /** Loading Loading @@ -625,7 +628,7 @@ public class SpatializerHelper { } if (!knownDevice) { mSADevices.add(new SADeviceState(ada.getType(), ada.getAddress())); //### TODO persist list mAudioService.persistSpatialAudioDeviceSettings(); } } Loading Loading @@ -1059,6 +1062,7 @@ public class SpatializerHelper { } Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada); deviceState.mHeadTrackerEnabled = enabled; mAudioService.persistSpatialAudioDeviceSettings(); break; } } Loading Loading @@ -1097,7 +1101,10 @@ public class SpatializerHelper { if (deviceType == deviceState.mDeviceType && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) || !wireless) { if (!deviceState.mHasHeadTracker) { deviceState.mHasHeadTracker = true; mAudioService.persistSpatialAudioDeviceSettings(); } return deviceState.mHeadTrackerEnabled; } } Loading Loading @@ -1457,16 +1464,45 @@ public class SpatializerHelper { } } private static final class SADeviceState { /*package*/ static final class SADeviceState { final @AudioDeviceInfo.AudioDeviceType int mDeviceType; final @Nullable String mDeviceAddress; // non-null for wireless devices 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, @Nullable String address) { SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) { mDeviceType = deviceType; mDeviceAddress = address; 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 Loading @@ -1474,6 +1510,64 @@ public class SpatializerHelper { 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); } } } private static String spatStateString(int state) { Loading Loading @@ -1557,4 +1651,11 @@ public class SpatializerHelper { AudioService.sSpatialLogger.loglog(msg, AudioEventLogger.Event.ALOGE, TAG); return msg; } //------------------------------------------------ // for testing purposes only /*package*/ void clearSADevices() { mSADevices.clear(); } } services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java 0 → 100644 +137 −0 Original line number Diff line number Diff line /* * Copyright 2022 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 com.android.server.audio.SpatializerHelper.SADeviceState; 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.AudioSystem; import android.util.Log; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class SpatializerHelperTest { private static final String TAG = "SpatializerHelperTest"; // the actual class under test private SpatializerHelper mSpatHelper; @Mock private AudioService mMockAudioService; @Spy private AudioSystemAdapter mSpyAudioSystem; @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); } @Test public void testSADeviceSettings() throws Exception { Log.i(TAG, "starting testSADeviceSettings"); final AudioDeviceAttributes dev1 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""); final AudioDeviceAttributes dev2 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "C3:P0:beep"); final AudioDeviceAttributes dev3 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop"); doNothing().when(mMockAudioService).persistSpatialAudioDeviceSettings(); // test with single device mSpatHelper.addCompatibleAudioDevice(dev1); checkAddSettings(); // test with 2+ devices so separator character is used in list mSpatHelper.addCompatibleAudioDevice(dev2); Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); checkAddSettings(); Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); mSpatHelper.addCompatibleAudioDevice(dev3); checkAddSettings(); // test adding a device twice in the list mSpatHelper.addCompatibleAudioDevice(dev1); checkAddSettings(); // test removing a device mSpatHelper.removeCompatibleAudioDevice(dev2); // spatializer could still be run for dev2 (is available) but spatial audio // is disabled for dev2 by removeCompatibleAudioDevice Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); List<AudioDeviceAttributes> compatDevices = mSpatHelper.getCompatibleAudioDevices(); Assert.assertFalse(compatDevices.stream().anyMatch(dev -> dev.equalTypeAddress(dev2))); checkAddSettings(); } /** * Gets the string representing the current configuration of the devices, then clears it * and restores the configuration. Verify the new string from the restored settings matches * the original one. */ private void checkAddSettings() throws Exception { String settings = mSpatHelper.getSADeviceSettings(); Log.i(TAG, "device settings: " + settings); mSpatHelper.clearSADevices(); mSpatHelper.setSADeviceSettings(settings); String settingsRestored = mSpatHelper.getSADeviceSettings(); Log.i(TAG, "device settingsRestored: " + settingsRestored); Assert.assertEquals(settings, settingsRestored); } } Loading
services/core/java/com/android/server/audio/AudioService.java +45 −4 Original line number Diff line number Diff line Loading @@ -341,8 +341,7 @@ 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; // commented out for now, will be reused for other SA persisting //private static final int MSG_PERSIST_SPATIAL_AUDIO_ENABLED = 43; 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; Loading Loading @@ -8122,8 +8121,7 @@ public class AudioService extends IAudioService.Stub break; case MSG_INIT_SPATIALIZER: mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect); mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); onInitSpatializer(); mAudioEventWakeLock.release(); break; Loading @@ -8131,6 +8129,10 @@ 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; Loading Loading @@ -9099,6 +9101,45 @@ public class AudioService extends IAudioService.Stub /*arg1*/ 0, /*arg2*/ 0, TAG, /*delay*/ 0); } 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); } 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) || Loading
services/core/java/com/android/server/audio/SpatializerHelper.java +111 −10 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.media.Spatializer; import android.media.SpatializerHeadTrackingMode; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseIntArray; Loading @@ -48,6 +49,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; /** Loading Loading @@ -275,18 +277,17 @@ public class SpatializerHelper { // 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) { // TODO deal with persisted values // not force-enabling as this device might already be in the device list addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), false /*forceEnable*/); } if (mBinauralSupported) { // TODO deal with persisted values // not force-enabling as this device might already be in the device list addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), false /*forceEnable*/); } // TODO read persisted states } catch (RemoteException e) { resetCapabilities(); } finally { Loading Loading @@ -533,11 +534,12 @@ public class SpatializerHelper { } if (!isInList) { final SADeviceState dev = new SADeviceState(deviceType, wireless ? ada.getAddress() : null); wireless ? ada.getAddress() : ""); dev.mEnabled = true; mSADevices.add(dev); } onRoutingUpdated(); mAudioService.persistSpatialAudioDeviceSettings(); } synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { Loading @@ -553,6 +555,7 @@ public class SpatializerHelper { } } onRoutingUpdated(); mAudioService.persistSpatialAudioDeviceSettings(); } /** Loading Loading @@ -625,7 +628,7 @@ public class SpatializerHelper { } if (!knownDevice) { mSADevices.add(new SADeviceState(ada.getType(), ada.getAddress())); //### TODO persist list mAudioService.persistSpatialAudioDeviceSettings(); } } Loading Loading @@ -1059,6 +1062,7 @@ public class SpatializerHelper { } Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada); deviceState.mHeadTrackerEnabled = enabled; mAudioService.persistSpatialAudioDeviceSettings(); break; } } Loading Loading @@ -1097,7 +1101,10 @@ public class SpatializerHelper { if (deviceType == deviceState.mDeviceType && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) || !wireless) { if (!deviceState.mHasHeadTracker) { deviceState.mHasHeadTracker = true; mAudioService.persistSpatialAudioDeviceSettings(); } return deviceState.mHeadTrackerEnabled; } } Loading Loading @@ -1457,16 +1464,45 @@ public class SpatializerHelper { } } private static final class SADeviceState { /*package*/ static final class SADeviceState { final @AudioDeviceInfo.AudioDeviceType int mDeviceType; final @Nullable String mDeviceAddress; // non-null for wireless devices 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, @Nullable String address) { SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) { mDeviceType = deviceType; mDeviceAddress = address; 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 Loading @@ -1474,6 +1510,64 @@ public class SpatializerHelper { 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); } } } private static String spatStateString(int state) { Loading Loading @@ -1557,4 +1651,11 @@ public class SpatializerHelper { AudioService.sSpatialLogger.loglog(msg, AudioEventLogger.Event.ALOGE, TAG); return msg; } //------------------------------------------------ // for testing purposes only /*package*/ void clearSADevices() { mSADevices.clear(); } }
services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java 0 → 100644 +137 −0 Original line number Diff line number Diff line /* * Copyright 2022 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 com.android.server.audio.SpatializerHelper.SADeviceState; 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.AudioSystem; import android.util.Log; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class SpatializerHelperTest { private static final String TAG = "SpatializerHelperTest"; // the actual class under test private SpatializerHelper mSpatHelper; @Mock private AudioService mMockAudioService; @Spy private AudioSystemAdapter mSpyAudioSystem; @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); } @Test public void testSADeviceSettings() throws Exception { Log.i(TAG, "starting testSADeviceSettings"); final AudioDeviceAttributes dev1 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""); final AudioDeviceAttributes dev2 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "C3:P0:beep"); final AudioDeviceAttributes dev3 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop"); doNothing().when(mMockAudioService).persistSpatialAudioDeviceSettings(); // test with single device mSpatHelper.addCompatibleAudioDevice(dev1); checkAddSettings(); // test with 2+ devices so separator character is used in list mSpatHelper.addCompatibleAudioDevice(dev2); Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); checkAddSettings(); Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); mSpatHelper.addCompatibleAudioDevice(dev3); checkAddSettings(); // test adding a device twice in the list mSpatHelper.addCompatibleAudioDevice(dev1); checkAddSettings(); // test removing a device mSpatHelper.removeCompatibleAudioDevice(dev2); // spatializer could still be run for dev2 (is available) but spatial audio // is disabled for dev2 by removeCompatibleAudioDevice Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2)); List<AudioDeviceAttributes> compatDevices = mSpatHelper.getCompatibleAudioDevices(); Assert.assertFalse(compatDevices.stream().anyMatch(dev -> dev.equalTypeAddress(dev2))); checkAddSettings(); } /** * Gets the string representing the current configuration of the devices, then clears it * and restores the configuration. Verify the new string from the restored settings matches * the original one. */ private void checkAddSettings() throws Exception { String settings = mSpatHelper.getSADeviceSettings(); Log.i(TAG, "device settings: " + settings); mSpatHelper.clearSADevices(); mSpatHelper.setSADeviceSettings(settings); String settingsRestored = mSpatHelper.getSADeviceSettings(); Log.i(TAG, "device settingsRestored: " + settingsRestored); Assert.assertEquals(settings, settingsRestored); } }