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

Commit 2aa59429 authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

Merge "Hide spatial audio toggle when disconnected" into main

parents b571f5ea 6f9a18ec
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`(