Merge "AudioService: persist/restore audio device spatial audio settings" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
11782ee969
@@ -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) ||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user