Hide spatial audio toggle when disconnected

BUG: 375546672
Test: atest SpatialAudioInteractorTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: I524174e6ef66b5d1ef90ac171c66f05aa7e26b53
This commit is contained in:
Haijie Hong
2024-10-28 15:51:11 +08:00
parent 75e2dc4b21
commit 6f9a18ec68
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 com.android.settingslib.media.domain.interactor.SpatializerInteractor
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted 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.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -41,9 +44,7 @@ import kotlinx.coroutines.launch
/** Provides device setting for spatial audio. */ /** Provides device setting for spatial audio. */
interface SpatialAudioInteractor { interface SpatialAudioInteractor {
/** Gets device setting for spatial audio */ /** Gets device setting for spatial audio */
fun getDeviceSetting( fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?>
cachedDevice: CachedBluetoothDevice,
): Flow<DeviceSettingModel?>
} }
class SpatialAudioInteractorImpl( class SpatialAudioInteractorImpl(
@@ -56,33 +57,55 @@ class SpatialAudioInteractorImpl(
private val spatialAudioOffToggle = private val spatialAudioOffToggle =
ToggleModel( ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_off), 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 = private val spatialAudioOnToggle =
ToggleModel( ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_on), 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 = private val headTrackingOnToggle =
ToggleModel( ToggleModel(
context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on), 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>() private val changes = MutableSharedFlow<Unit>()
override fun getDeviceSetting( override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?> =
cachedDevice: CachedBluetoothDevice,
): Flow<DeviceSettingModel?> =
changes changes
.onStart { emit(Unit) } .onStart { emit(Unit) }
.map { getSpatialAudioDeviceSettingModel(cachedDevice) } .combine(
isDeviceConnected(cachedDevice),
) { _, connected ->
if (connected) {
getSpatialAudioDeviceSettingModel(cachedDevice)
} else {
null
}
}
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null) .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( private suspend fun getSpatialAudioDeviceSettingModel(
cachedDevice: CachedBluetoothDevice, cachedDevice: CachedBluetoothDevice
): DeviceSettingModel? { ): DeviceSettingModel? {
// TODO(b/343317785): use audio repository instead of calling AudioManager directly. // TODO(b/343317785): use audio repository instead of calling AudioManager directly.
Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}") Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}")
val attributes = val attributes =
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio( BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
cachedDevice, audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address)) cachedDevice,
audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address),
)
?: run { ?: run {
Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.") Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.")
return null return null
@@ -116,7 +139,8 @@ class SpatialAudioInteractorImpl(
TAG, TAG,
"Head tracking available: $headTrackingAvailable, " + "Head tracking available: $headTrackingAvailable, " +
"spatial audio enabled: $spatialAudioEnabled, " + "spatial audio enabled: $spatialAudioEnabled, " +
"head tracking enabled: $headTrackingEnabled") "head tracking enabled: $headTrackingEnabled",
)
return DeviceSettingModel.MultiTogglePreference( return DeviceSettingModel.MultiTogglePreference(
cachedDevice = cachedDevice, cachedDevice = cachedDevice,
id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE, id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE,
@@ -143,7 +167,8 @@ class SpatialAudioInteractorImpl(
} }
changes.emit(Unit) changes.emit(Unit)
} }
}) },
)
} }
companion object { companion object {

View File

@@ -83,6 +83,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_noAudioProfile_returnNull() { fun getDeviceSetting_noAudioProfile_returnNull() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
assertThat(setting).isNull() assertThat(setting).isNull()
@@ -93,6 +94,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_audioProfileNotEnabled_returnNull() { fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false) `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 @Test
fun getDeviceSetting_spatialAudioNotSupported_returnNull() { fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`( `when`(
@@ -122,6 +139,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() { fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`( `when`(
@@ -150,6 +168,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_headTrackingSupported_returnThreeToggles() { fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`( `when`(
@@ -178,6 +197,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_updateState_enableSpatialAudio() { fun getDeviceSetting_updateState_enableSpatialAudio() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`( `when`(
@@ -207,6 +227,7 @@ class SpatialAudioInteractorTest {
@Test @Test
fun getDeviceSetting_updateState_enableHeadTracking() { fun getDeviceSetting_updateState_enableHeadTracking() {
testScope.runTest { testScope.runTest {
`when`(cachedDevice.isConnected).thenReturn(true)
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
`when`( `when`(