Merge "Implement Spatial audio toggle domain layer" into main
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user