Merge "AudioService: persist/restore audio device spatial audio settings" into tm-dev am: 11782ee969

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

Change-Id: I239f8db14a769d10caa05e97f9be2765f4fbcf3f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
TreeHugger Robot
2022-05-18 20:58:19 +00:00
committed by Automerger Merge Worker
3 changed files with 293 additions and 14 deletions

View File

@@ -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;
@@ -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;
@@ -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;
@@ -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) ||

View File

@@ -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;
@@ -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;
/**
@@ -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 {
@@ -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) {
@@ -553,6 +555,7 @@ public class SpatializerHelper {
}
}
onRoutingUpdated();
mAudioService.persistSpatialAudioDeviceSettings();
}
/**
@@ -625,7 +628,7 @@ public class SpatializerHelper {
}
if (!knownDevice) {
mSADevices.add(new SADeviceState(ada.getType(), ada.getAddress()));
//### TODO persist list
mAudioService.persistSpatialAudioDeviceSettings();
}
}
@@ -1059,6 +1062,7 @@ public class SpatializerHelper {
}
Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
deviceState.mHeadTrackerEnabled = enabled;
mAudioService.persistSpatialAudioDeviceSettings();
break;
}
}
@@ -1097,7 +1101,10 @@ public class SpatializerHelper {
if (deviceType == deviceState.mDeviceType
&& (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
|| !wireless) {
deviceState.mHasHeadTracker = true;
if (!deviceState.mHasHeadTracker) {
deviceState.mHasHeadTracker = true;
mAudioService.persistSpatialAudioDeviceSettings();
}
return deviceState.mHeadTrackerEnabled;
}
}
@@ -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
@@ -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) {
@@ -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();
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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);
}
}