diff --git a/res/values/strings.xml b/res/values/strings.xml index cd5f71aafae..eb6e0129049 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8561,6 +8561,24 @@ Automatically caption media + + Phone speaker + + + Wired headphones + + + Spatial Audio creates immersive sound that seems like it’s coming from all around you. Only works with some media. + + + Off + + + On / %1$s + + + On / %1$s and %2$s + {count, plural, diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml index d79594c19a9..f25b6ec54b0 100644 --- a/res/xml/sound_settings.xml +++ b/res/xml/sound_settings.xml @@ -111,6 +111,15 @@ settings:keywords="@string/sound_settings"/> + + + + - - - + + + + + + + + + + + diff --git a/src/com/android/settings/notification/SpatialAudioParentPreferenceController.java b/src/com/android/settings/notification/SpatialAudioParentPreferenceController.java new file mode 100644 index 00000000000..c9eaa65d76d --- /dev/null +++ b/src/com/android/settings/notification/SpatialAudioParentPreferenceController.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 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.settings.notification; + +import android.content.Context; +import android.media.AudioManager; +import android.media.Spatializer; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +/** + * Parent menu summary of the Spatial audio settings + */ +public class SpatialAudioParentPreferenceController extends BasePreferenceController { + private static final String TAG = "SpatialAudioSetting"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Spatializer mSpatializer; + private SpatialAudioPreferenceController mSpatialAudioPreferenceController; + private SpatialAudioWiredHeadphonesController mSpatialAudioWiredHeadphonesController; + + public SpatialAudioParentPreferenceController(Context context, String key) { + super(context, key); + AudioManager audioManager = context.getSystemService(AudioManager.class); + mSpatializer = audioManager.getSpatializer(); + mSpatialAudioPreferenceController = new SpatialAudioPreferenceController(context, "unused"); + mSpatialAudioWiredHeadphonesController = new SpatialAudioWiredHeadphonesController(context, + "unused"); + } + + @Override + public int getAvailabilityStatus() { + int level = mSpatializer.getImmersiveAudioLevel(); + if (DEBUG) { + Log.d(TAG, "spatialization level: " + level); + } + return level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE + ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + } + + @Override + public CharSequence getSummary() { + boolean speakerOn = mSpatialAudioPreferenceController.isAvailable() + && mSpatialAudioWiredHeadphonesController.isChecked(); + boolean wiredHeadphonesOn = mSpatialAudioWiredHeadphonesController.isAvailable() + && mSpatialAudioWiredHeadphonesController.isChecked(); + if (speakerOn && wiredHeadphonesOn) { + return mContext.getString(R.string.spatial_summary_on_two, + mContext.getString(R.string.spatial_audio_speaker), + mContext.getString(R.string.spatial_audio_wired_headphones)); + } else if (speakerOn) { + return mContext.getString(R.string.spatial_summary_on_one, + mContext.getString(R.string.spatial_audio_speaker)); + } else if (wiredHeadphonesOn) { + return mContext.getString(R.string.spatial_summary_on_one, + mContext.getString(R.string.spatial_audio_wired_headphones)); + } else { + return mContext.getString(R.string.spatial_summary_off); + } + } +} diff --git a/src/com/android/settings/notification/SpatialAudioPreferenceController.java b/src/com/android/settings/notification/SpatialAudioPreferenceController.java index 7bca516f1c3..707340f0dd7 100644 --- a/src/com/android/settings/notification/SpatialAudioPreferenceController.java +++ b/src/com/android/settings/notification/SpatialAudioPreferenceController.java @@ -17,46 +17,55 @@ package com.android.settings.notification; import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.Spatializer; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; /** - * The controller of the Spatial audio setting in the SoundSettings. + * The controller of the Spatial audio setting for speaker in the SoundSettings. */ public class SpatialAudioPreferenceController extends TogglePreferenceController { - private static final String KEY_SPATIAL_AUDIO = "spatial_audio"; - private final Spatializer mSpatializer; + @VisibleForTesting + final AudioDeviceAttributes mSpeaker = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "" + ); - public SpatialAudioPreferenceController(Context context) { - super(context, KEY_SPATIAL_AUDIO); + public SpatialAudioPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); AudioManager audioManager = context.getSystemService(AudioManager.class); mSpatializer = audioManager.getSpatializer(); } @Override public int getAvailabilityStatus() { - return mSpatializer.getImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE - ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + return mSpatializer.isAvailableForDevice(mSpeaker) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override public boolean isChecked() { - return mSpatializer.isEnabled(); + return mSpatializer.getCompatibleAudioDevices().contains(mSpeaker); } @Override public boolean setChecked(boolean isChecked) { - mSpatializer.setEnabled(isChecked); + if (isChecked) { + mSpatializer.addCompatibleAudioDevice(mSpeaker); + } else { + mSpatializer.removeCompatibleAudioDevice(mSpeaker); + } return isChecked == isChecked(); } @Override public int getSliceHighlightMenuRes() { - return R.string.menu_key_notifications; + return R.string.menu_key_sound; } } diff --git a/src/com/android/settings/notification/SpatialAudioSettings.java b/src/com/android/settings/notification/SpatialAudioSettings.java new file mode 100644 index 00000000000..001c6176dbc --- /dev/null +++ b/src/com/android/settings/notification/SpatialAudioSettings.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 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.settings.notification; + +import android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** + * Spatial audio settings located in the sound menu + */ +@SearchIndexable +public class SpatialAudioSettings extends DashboardFragment { + + private static final String TAG = "SpatialAudioSettings"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_SPATIAL_AUDIO; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.spatial_audio_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.spatial_audio_settings); +} diff --git a/src/com/android/settings/notification/SpatialAudioWiredHeadphonesController.java b/src/com/android/settings/notification/SpatialAudioWiredHeadphonesController.java new file mode 100644 index 00000000000..9ff6a7f6f3f --- /dev/null +++ b/src/com/android/settings/notification/SpatialAudioWiredHeadphonesController.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 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.settings.notification; + +import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.Spatializer; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; + +/** + * The controller of the Spatial audio setting for wired headphones in the SoundSettings. + */ +public class SpatialAudioWiredHeadphonesController extends TogglePreferenceController { + + private final Spatializer mSpatializer; + @VisibleForTesting + final AudioDeviceAttributes mWiredHeadphones = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_WIRED_HEADPHONES, "" + ); + + public SpatialAudioWiredHeadphonesController(Context context, String preferenceKey) { + super(context, preferenceKey); + AudioManager audioManager = context.getSystemService(AudioManager.class); + mSpatializer = audioManager.getSpatializer(); + } + + @Override + public int getAvailabilityStatus() { + return mSpatializer.isAvailableForDevice(mWiredHeadphones) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isChecked() { + return mSpatializer.getCompatibleAudioDevices().contains(mWiredHeadphones); + } + + @Override + public boolean setChecked(boolean isChecked) { + if (isChecked) { + mSpatializer.addCompatibleAudioDevice(mWiredHeadphones); + } else { + mSpatializer.removeCompatibleAudioDevice(mWiredHeadphones); + } + return isChecked == isChecked(); + } + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_sound; + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SpatialAudioParentPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SpatialAudioParentPreferenceControllerTest.java new file mode 100644 index 00000000000..21fcea38276 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SpatialAudioParentPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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.settings.notification; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.AudioManager; +import android.media.Spatializer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class SpatialAudioParentPreferenceControllerTest { + + private static final String KEY = "spatial_audio_summary"; + + @Mock + private Context mContext; + @Mock + private AudioManager mAudioManager; + @Mock + private Spatializer mSpatializer; + + private SpatialAudioParentPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + when(mAudioManager.getSpatializer()).thenReturn(mSpatializer); + mController = new SpatialAudioParentPreferenceController(mContext, KEY); + } + + @Test + public void getAvailabilityStatus_levelNone_shouldReturnUnsupported() { + when(mSpatializer.getImmersiveAudioLevel()).thenReturn( + Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_levelMultiChannel_shouldReturnAvailable() { + when(mSpatializer.getImmersiveAudioLevel()).thenReturn( + Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SpatialAudioPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SpatialAudioPreferenceControllerTest.java index 66d18ec8308..e88b75872f6 100644 --- a/tests/robotests/src/com/android/settings/notification/SpatialAudioPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/SpatialAudioPreferenceControllerTest.java @@ -30,7 +30,6 @@ import android.media.AudioManager; import android.media.Spatializer; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -39,7 +38,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -@Ignore("b/200896161") @RunWith(RobolectricTestRunner.class) public class SpatialAudioPreferenceControllerTest { @@ -56,36 +54,36 @@ public class SpatialAudioPreferenceControllerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - when((Object) mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); when(mAudioManager.getSpatializer()).thenReturn(mSpatializer); - mController = new SpatialAudioPreferenceController(mContext); + mController = new SpatialAudioPreferenceController(mContext, "unused"); } @Test - public void getAvailabilityStatus_levelNone_shouldReturnUnsupported() { - when(mSpatializer.getImmersiveAudioLevel()).thenReturn( - Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE); + public void getAvailabilityStatus_unavailable_shouldReturnUnavailable() { + when(mSpatializer.isAvailableForDevice(mController.mSpeaker)).thenReturn(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); } @Test - public void getAvailabilityStatus_levelMultiChannel_shouldReturnAvailable() { - when(mSpatializer.getImmersiveAudioLevel()).thenReturn( - Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL); + public void getAvailabilityStatus_available_shouldReturnAvailable() { + when(mSpatializer.isAvailableForDevice(mController.mSpeaker)).thenReturn(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); } @Test - public void setChecked_withTrue_shouldEnableSpatializer() { + public void setChecked_withTrue_enablesDeviceSpatializer() { mController.setChecked(true); - verify(mSpatializer).setEnabled(true); + verify(mSpatializer).addCompatibleAudioDevice(mController.mSpeaker); } @Test - public void setChecked_withFalse_shouldDisableSpatializer() { + public void setChecked_withFalse_disablesDeviceSpatializer() { mController.setChecked(false); - verify(mSpatializer).setEnabled(false); + verify(mSpatializer).removeCompatibleAudioDevice(mController.mSpeaker); } } diff --git a/tests/robotests/src/com/android/settings/notification/SpatialAudioWiredHeadphonesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SpatialAudioWiredHeadphonesPreferenceControllerTest.java new file mode 100644 index 00000000000..14d70cab815 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SpatialAudioWiredHeadphonesPreferenceControllerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 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.settings.notification; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.AudioManager; +import android.media.Spatializer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class SpatialAudioWiredHeadphonesPreferenceControllerTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock + private AudioManager mAudioManager; + @Mock + private Spatializer mSpatializer; + + private SpatialAudioWiredHeadphonesController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + when(mAudioManager.getSpatializer()).thenReturn(mSpatializer); + mController = new SpatialAudioWiredHeadphonesController(mContext, "unused"); + } + + @Test + public void getAvailabilityStatus_unavailable_shouldReturnUnavailable() { + when(mSpatializer.isAvailableForDevice(mController.mWiredHeadphones)).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_available_shouldReturnAvailable() { + when(mSpatializer.isAvailableForDevice(mController.mWiredHeadphones)).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void setChecked_withTrue_enablesDeviceSpatializer() { + mController.setChecked(true); + + verify(mSpatializer).addCompatibleAudioDevice(mController.mWiredHeadphones); + } + + @Test + public void setChecked_withFalse_disablesDeviceSpatializer() { + mController.setChecked(false); + + verify(mSpatializer).removeCompatibleAudioDevice(mController.mWiredHeadphones); + } +}