Merge "Hide spatial audio toggle when disconnected" into main

This commit is contained in:
Haijie Hong
2024-10-29 03:03:42 +00:00
committed by Android (Google) Code Review
2 changed files with 61 additions and 15 deletions

View File

@@ -30,10 +30,13 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -41,9 +44,7 @@ import kotlinx.coroutines.launch
/** Provides device setting for spatial audio. */
interface SpatialAudioInteractor {
/** Gets device setting for spatial audio */
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
): Flow<DeviceSettingModel?>
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?>
}
class SpatialAudioInteractorImpl(
@@ -56,33 +57,55 @@ class SpatialAudioInteractorImpl(
private val spatialAudioOffToggle =
ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_off),
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off))
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off),
)
private val spatialAudioOnToggle =
ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_on),
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio))
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio),
)
private val headTrackingOnToggle =
ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on),
DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking))
DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking),
)
private val changes = MutableSharedFlow<Unit>()
override fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
): Flow<DeviceSettingModel?> =
override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?> =
changes
.onStart { emit(Unit) }
.map { getSpatialAudioDeviceSettingModel(cachedDevice) }
.combine(
isDeviceConnected(cachedDevice),
) { _, connected ->
if (connected) {
getSpatialAudioDeviceSettingModel(cachedDevice)
} else {
null
}
}
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null)
private fun isDeviceConnected(cachedDevice: CachedBluetoothDevice): Flow<Boolean> =
callbackFlow {
val listener =
CachedBluetoothDevice.Callback { launch { send(cachedDevice.isConnected) } }
cachedDevice.registerCallback(context.mainExecutor, listener)
awaitClose { cachedDevice.unregisterCallback(listener) }
}
.onStart { emit(cachedDevice.isConnected) }
.flowOn(backgroundCoroutineContext)
private suspend fun getSpatialAudioDeviceSettingModel(
cachedDevice: CachedBluetoothDevice,
cachedDevice: CachedBluetoothDevice
): DeviceSettingModel? {
// TODO(b/343317785): use audio repository instead of calling AudioManager directly.
Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}")
val attributes =
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
cachedDevice, audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address))
cachedDevice,
audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address),
)
?: run {
Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.")
return null
@@ -116,7 +139,8 @@ class SpatialAudioInteractorImpl(
TAG,
"Head tracking available: $headTrackingAvailable, " +
"spatial audio enabled: $spatialAudioEnabled, " +
"head tracking enabled: $headTrackingEnabled")
"head tracking enabled: $headTrackingEnabled",
)
return DeviceSettingModel.MultiTogglePreference(
cachedDevice = cachedDevice,
id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE,
@@ -143,7 +167,8 @@ class SpatialAudioInteractorImpl(
}
changes.emit(Unit)
}
})
},
)
}
companion object {

View File

@@ -83,6 +83,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_noAudioProfile_returnNull() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull()
@@ -93,6 +94,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
@@ -103,9 +105,24 @@ class SpatialAudioInteractorTest {
}
}
@Test
fun getDeviceSetting_deviceNotConnected_returnNull() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(false)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull()
verifyNoInteractions(spatializerRepository)
}
}
@Test
fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
@@ -122,6 +139,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
@@ -150,6 +168,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
@@ -178,6 +197,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_updateState_enableSpatialAudio() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(
@@ -207,6 +227,7 @@ class SpatialAudioInteractorTest {
@Test
fun getDeviceSetting_updateState_enableHeadTracking() {
testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`(