Merge "Implement Spatial audio toggle domain layer" into main

This commit is contained in:
Haijie Hong
2024-08-15 10:54:48 +00:00
committed by Android (Google) Code Review
17 changed files with 825 additions and 257 deletions

View File

@@ -0,0 +1,254 @@
/*
* Copyright (C) 2024 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.bluetooth.domain.interactor
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.media.AudioDeviceAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import androidx.test.core.app.ApplicationProvider
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LeAudioProfile
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.media.data.repository.SpatializerRepository
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.RobolectricTestRunner
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class SpatialAudioInteractorTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var audioManager: AudioManager
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothDevice: BluetoothDevice
@Mock private lateinit var spatializerRepository: SpatializerRepository
@Mock private lateinit var leAudioProfile: LeAudioProfile
private lateinit var underTest: SpatialAudioInteractor
private val testScope = TestScope()
@Before
fun setUp() {
val context = spy(ApplicationProvider.getApplicationContext<Context>())
`when`(cachedDevice.device).thenReturn(bluetoothDevice)
`when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
`when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO)
underTest =
SpatialAudioInteractorImpl(
context,
audioManager,
SpatializerInteractor(spatializerRepository),
testScope.backgroundScope,
testScope.testScheduler)
}
@Test
fun getDeviceSetting_noAudioProfile_returnNull() {
testScope.runTest {
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull()
verifyNoInteractions(spatializerRepository)
}
}
@Test
fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull()
verifyNoInteractions(spatializerRepository)
}
}
@Test
fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
spatializerRepository.isSpatialAudioAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(false)
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull()
}
}
@Test
fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
spatializerRepository.isSpatialAudioAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(
spatializerRepository.isHeadTrackingAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(false)
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(false)
val setting =
getLatestValue(underTest.getDeviceSetting(cachedDevice))
as DeviceSettingModel.MultiTogglePreference
assertThat(setting).isNotNull()
assertThat(setting.toggles.size).isEqualTo(2)
assertThat(setting.state.selectedIndex).isEqualTo(1)
}
}
@Test
fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
spatializerRepository.isSpatialAudioAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(
spatializerRepository.isHeadTrackingAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
val setting =
getLatestValue(underTest.getDeviceSetting(cachedDevice))
as DeviceSettingModel.MultiTogglePreference
assertThat(setting).isNotNull()
assertThat(setting.toggles.size).isEqualTo(3)
assertThat(setting.state.selectedIndex).isEqualTo(2)
}
}
@Test
fun getDeviceSetting_updateState_enableSpatialAudio() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
spatializerRepository.isSpatialAudioAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(
spatializerRepository.isHeadTrackingAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(false)
val setting =
getLatestValue(underTest.getDeviceSetting(cachedDevice))
as DeviceSettingModel.MultiTogglePreference
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
runCurrent()
assertThat(setting).isNotNull()
verify(spatializerRepository, times(1))
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
}
}
@Test
fun getDeviceSetting_updateState_enableHeadTracking() {
testScope.runTest {
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
spatializerRepository.isSpatialAudioAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(
spatializerRepository.isHeadTrackingAvailableForDevice(
BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(true)
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
.thenReturn(false)
val setting =
getLatestValue(underTest.getDeviceSetting(cachedDevice))
as DeviceSettingModel.MultiTogglePreference
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
runCurrent()
assertThat(setting).isNotNull()
verify(spatializerRepository, times(1))
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
verify(spatializerRepository, times(1))
.setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true)
}
}
private fun getLatestValue(deviceSettingFlow: Flow<DeviceSettingModel?>): DeviceSettingModel? {
var latestValue: DeviceSettingModel? = null
deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope)
testScope.runCurrent()
return latestValue
}
private companion object {
const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34"
val BLE_AUDIO_DEVICE_ATTRIBUTES =
AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLE_HEADSET,
BLUETOOTH_ADDRESS,
)
}
}

View File

@@ -19,11 +19,13 @@ package com.android.settings.bluetooth.ui.view
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.graphics.Bitmap
import android.media.AudioManager
import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -31,6 +33,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
@@ -45,6 +48,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -59,6 +63,7 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
private lateinit var fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter
@@ -73,6 +78,10 @@ class DeviceDetailsFragmentFormatterTest {
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
eq(context), eq(bluetoothAdapter), any()))
.thenReturn(repository)
`when`(
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
eq(context), any(AudioManager::class.java), any()))
.thenReturn(spatialAudioInteractor)
val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
assertThat(fragmentActivity.applicationContext).isNotNull()
fragment = TestFragment(context)
@@ -186,7 +195,15 @@ class DeviceDetailsFragmentFormatterTest {
toggles =
listOf(
ToggleModel(
"", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
"", DeviceSettingIcon.BitmapIcon(
Bitmap.createBitmap(
1,
1,
Bitmap.Config.ARGB_8888
)
)
)
),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,

View File

@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -27,6 +28,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
@@ -45,6 +47,8 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -61,6 +65,8 @@ class BluetoothDeviceDetailsViewModelTest {
@Mock private lateinit var repository: DeviceSettingRepository
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
private lateinit var underTest: BluetoothDeviceDetailsViewModel
private lateinit var featureFactory: FakeFeatureFactory
private val testScope = TestScope()
@@ -74,7 +80,8 @@ class BluetoothDeviceDetailsViewModelTest {
eq(context), eq(bluetoothAdapter), any()))
.thenReturn(repository)
underTest = BluetoothDeviceDetailsViewModel(repository, cachedDevice)
underTest =
BluetoothDeviceDetailsViewModel(repository, spatialAudioInteractor, cachedDevice)
}
@Test
@@ -91,6 +98,66 @@ class BluetoothDeviceDetailsViewModelTest {
}
}
@Test
fun getDeviceSetting_returnRepositoryResponse() {
testScope.runTest {
val remoteSettingId1 = 10001
val pref = buildMultiTogglePreference(remoteSettingId1)
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
BUILTIN_SETTING_ITEM_1,
buildRemoteSettingItem(remoteSettingId1),
),
listOf(),
"footer"))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
.thenReturn(flowOf(pref))
var deviceSetting: DeviceSettingModel? = null
underTest
.getDeviceSetting(cachedDevice, remoteSettingId1)
.onEach { deviceSetting = it }
.launchIn(testScope.backgroundScope)
runCurrent()
assertThat(deviceSetting).isSameInstanceAs(pref)
verify(repository, times(1)).getDeviceSetting(cachedDevice, remoteSettingId1)
}
}
@Test
fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() {
testScope.runTest {
val pref =
buildMultiTogglePreference(
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
BUILTIN_SETTING_ITEM_1,
buildRemoteSettingItem(
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE),
),
listOf(),
"footer"))
`when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref))
var deviceSetting: DeviceSettingModel? = null
underTest
.getDeviceSetting(
cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
.onEach { deviceSetting = it }
.launchIn(testScope.backgroundScope)
runCurrent()
assertThat(deviceSetting).isSameInstanceAs(pref)
verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice)
}
}
@Test
fun getLayout_builtinDeviceSettings() {
testScope.runTest {
@@ -163,7 +230,12 @@ class BluetoothDeviceDetailsViewModelTest {
cachedDevice,
settingId,
"title",
toggles = listOf(ToggleModel("", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
toggles =
listOf(
ToggleModel(
"toggle1",
DeviceSettingIcon.BitmapIcon(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,