diff --git a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt index 4b91716a4b4..cade566be6d 100644 --- a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt +++ b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt @@ -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 + fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow } 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() - override fun getDeviceSetting( - cachedDevice: CachedBluetoothDevice, - ): Flow = + override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow = 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 = + 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 { diff --git a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt index a83b7c2780e..28e05810467 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt @@ -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`(