Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6f9a18ec authored by Haijie Hong's avatar Haijie Hong
Browse files

Hide spatial audio toggle when disconnected

BUG: 375546672
Test: atest SpatialAudioInteractorTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: I524174e6ef66b5d1ef90ac171c66f05aa7e26b53
parent 75e2dc4b
Loading
Loading
Loading
Loading
+40 −15
Original line number Diff line number Diff line
@@ -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 {
+21 −0
Original line number Diff line number Diff line
@@ -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`(