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:
@@ -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 {
|
||||||
|
|||||||
@@ -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`(
|
||||||
|
|||||||
Reference in New Issue
Block a user