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

Commit 765df2d8 authored by Haijie Hong's avatar Haijie Hong
Browse files

Determine Spatial Audio AudioDeviceAttributes by BT profile state

Test: atest SpatialAudioComponentInteractorTest
Flag: com.android.settingslib.flags.enable_determining_spatial_audio_attributes_by_profile
Bug: 341005211
Change-Id: Ia9dc2463c412963b15631e82f1f6dd08dcf7c133
parent 98264c6c
Loading
Loading
Loading
Loading
+10 −0
Original line number Original line Diff line number Diff line
@@ -79,3 +79,13 @@ flag {
        purpose: PURPOSE_BUGFIX
        purpose: PURPOSE_BUGFIX
    }
    }
}
}

flag {
    name: "enable_determining_spatial_audio_attributes_by_profile"
    namespace: "cross_device_experiences"
    description: "Use bluetooth profile connection policy to determine spatial audio attributes"
    bug: "341005211"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+12 −0
Original line number Original line Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.ContentResolver
import android.database.ContentObserver
import android.database.ContentObserver
import android.media.AudioDeviceInfo
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager
import android.media.AudioManager.AudioDeviceCategory
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import android.provider.Settings
import android.provider.Settings
import androidx.concurrent.futures.DirectExecutor
import androidx.concurrent.futures.DirectExecutor
@@ -85,6 +86,10 @@ interface AudioRepository {
    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean): Boolean
    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean): Boolean


    suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)
    suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)

    /** Gets audio device category. */
    @AudioDeviceCategory
    suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int
}
}


class AudioRepositoryImpl(
class AudioRepositoryImpl(
@@ -211,6 +216,13 @@ class AudioRepositoryImpl(
        withContext(backgroundCoroutineContext) { audioManager.ringerMode = mode.value }
        withContext(backgroundCoroutineContext) { audioManager.ringerMode = mode.value }
    }
    }


    @AudioDeviceCategory
    override suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int {
        return withContext(backgroundCoroutineContext) {
            audioManager.getBluetoothAudioDeviceCategory(bluetoothAddress)
        }
    }

    private fun getMinVolume(stream: AudioStream): Int =
    private fun getMinVolume(stream: AudioStream): Int =
        try {
        try {
            audioManager.getStreamMinVolume(stream.value)
            audioManager.getStreamMinVolume(stream.value)
+13 −0
Original line number Original line Diff line number Diff line
@@ -247,6 +247,19 @@ class AudioRepositoryTest {
        }
        }
    }
    }


    @Test
    fun getBluetoothAudioDeviceCategory() {
        testScope.runTest {
            `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78")).thenReturn(
                AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)

            val category = underTest.getBluetoothAudioDeviceCategory("12:34:56:78")
            runCurrent()

            assertThat(category).isEqualTo(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)
        }
    }

    private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
    private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
        verify(audioManager)
        verify(audioManager)
            .addOnCommunicationDeviceChangedListener(
            .addOnCommunicationDeviceChangedListener(
+4 −0
Original line number Original line Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.systemui.volume.panel.component.spatial
package com.android.systemui.volume.panel.component.spatial


import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.spatializerInteractor
import com.android.systemui.media.spatializerInteractor
import com.android.systemui.volume.data.repository.audioRepository
import com.android.systemui.volume.domain.interactor.audioOutputInteractor
import com.android.systemui.volume.domain.interactor.audioOutputInteractor
import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor
import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor


@@ -27,6 +29,8 @@ val Kosmos.spatialAudioComponentInteractor by
        SpatialAudioComponentInteractor(
        SpatialAudioComponentInteractor(
            audioOutputInteractor,
            audioOutputInteractor,
            spatializerInteractor,
            spatializerInteractor,
            audioRepository,
            backgroundCoroutineContext,
            testScope.backgroundScope
            testScope.backgroundScope
        )
        )
    }
    }
+64 −6
Original line number Original line Diff line number Diff line
@@ -16,14 +16,22 @@


package com.android.systemui.volume.panel.component.spatial.domain.interactor
package com.android.systemui.volume.panel.component.spatial.domain.interactor


import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothProfile
import android.media.AudioDeviceAttributes
import android.media.AudioDeviceAttributes
import android.media.AudioDeviceInfo
import android.media.AudioDeviceInfo
import android.media.session.MediaSession
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.media.session.PlaybackState
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.TestableLooper
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.A2dpProfile
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.HearingAidProfile
import com.android.settingslib.bluetooth.LeAudioProfile
import com.android.settingslib.flags.Flags
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectLastValue
@@ -44,6 +52,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith


@@ -52,15 +61,29 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class SpatialAudioComponentInteractorTest : SysuiTestCase() {
class SpatialAudioComponentInteractorTest : SysuiTestCase() {
    @get:Rule val setFlagsRule = SetFlagsRule()


    private val kosmos = testKosmos()
    private val kosmos = testKosmos()
    private lateinit var underTest: SpatialAudioComponentInteractor
    private lateinit var underTest: SpatialAudioComponentInteractor
    private val a2dpProfile: A2dpProfile = mock {
        whenever(profileId).thenReturn(BluetoothProfile.A2DP)
    }
    private val leAudioProfile: LeAudioProfile = mock {
        whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
    }
    private val hearingAidProfile: HearingAidProfile = mock {
        whenever(profileId).thenReturn(BluetoothProfile.HEARING_AID)
    }
    private val bluetoothDevice: BluetoothDevice = mock {}


    @Before
    @Before
    fun setup() {
    fun setup() {
        with(kosmos) {
        with(kosmos) {
            val cachedBluetoothDevice: CachedBluetoothDevice = mock {
            val cachedBluetoothDevice: CachedBluetoothDevice = mock {
                whenever(address).thenReturn("test_address")
                whenever(address).thenReturn("test_address")
                whenever(device).thenReturn(bluetoothDevice)
                whenever(profiles)
                    .thenReturn(listOf(a2dpProfile, leAudioProfile, hearingAidProfile))
            }
            }
            localMediaRepository.updateCurrentConnectedDevice(
            localMediaRepository.updateCurrentConnectedDevice(
                mock<BluetoothMediaDevice> {
                mock<BluetoothMediaDevice> {
@@ -83,7 +106,7 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() {
    fun setEnabled_changesIsEnabled() {
    fun setEnabled_changesIsEnabled() {
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                spatializerRepository.setIsSpatialAudioAvailable(headset, true)
                spatializerRepository.setIsSpatialAudioAvailable(bleHeadsetAttributes, true)
                val values by collectValues(underTest.isEnabled)
                val values by collectValues(underTest.isEnabled)


                underTest.setEnabled(SpatialAudioEnabledModel.Disabled)
                underTest.setEnabled(SpatialAudioEnabledModel.Disabled)
@@ -105,11 +128,40 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() {
        }
        }
    }
    }


    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DETERMINING_SPATIAL_AUDIO_ATTRIBUTES_BY_PROFILE)
    fun setEnabled_determinedByBluetoothProfile_a2dpProfileEnabled() {
        with(kosmos) {
            testScope.runTest {
                whenever(a2dpProfile.isEnabled(bluetoothDevice)).thenReturn(true)
                whenever(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
                whenever(hearingAidProfile.isEnabled(bluetoothDevice)).thenReturn(false)
                spatializerRepository.setIsSpatialAudioAvailable(a2dpAttributes, true)
                val values by collectValues(underTest.isEnabled)

                underTest.setEnabled(SpatialAudioEnabledModel.Disabled)
                runCurrent()
                underTest.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled)
                runCurrent()

                assertThat(values)
                    .containsExactly(
                        SpatialAudioEnabledModel.Unknown,
                        SpatialAudioEnabledModel.Disabled,
                        SpatialAudioEnabledModel.SpatialAudioEnabled,
                    )
                    .inOrder()
                assertThat(spatializerRepository.getSpatialAudioCompatibleDevices())
                    .containsExactly(a2dpAttributes)
            }
        }
    }

    @Test
    @Test
    fun connectedDeviceSupports_isAvailable_SpatialAudio() {
    fun connectedDeviceSupports_isAvailable_SpatialAudio() {
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                spatializerRepository.setIsSpatialAudioAvailable(headset, true)
                spatializerRepository.setIsSpatialAudioAvailable(bleHeadsetAttributes, true)


                val isAvailable by collectLastValue(underTest.isAvailable)
                val isAvailable by collectLastValue(underTest.isAvailable)


@@ -123,8 +175,8 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() {
    fun connectedDeviceSupportsHeadTracking_isAvailable_HeadTracking() {
    fun connectedDeviceSupportsHeadTracking_isAvailable_HeadTracking() {
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                spatializerRepository.setIsSpatialAudioAvailable(headset, true)
                spatializerRepository.setIsSpatialAudioAvailable(bleHeadsetAttributes, true)
                spatializerRepository.setIsHeadTrackingAvailable(headset, true)
                spatializerRepository.setIsHeadTrackingAvailable(bleHeadsetAttributes, true)


                val isAvailable by collectLastValue(underTest.isAvailable)
                val isAvailable by collectLastValue(underTest.isAvailable)


@@ -138,7 +190,7 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() {
    fun connectedDeviceDoesntSupport_isAvailable_Unavailable() {
    fun connectedDeviceDoesntSupport_isAvailable_Unavailable() {
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                spatializerRepository.setIsSpatialAudioAvailable(headset, false)
                spatializerRepository.setIsSpatialAudioAvailable(bleHeadsetAttributes, false)


                val isAvailable by collectLastValue(underTest.isAvailable)
                val isAvailable by collectLastValue(underTest.isAvailable)


@@ -179,7 +231,13 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() {
    }
    }


    private companion object {
    private companion object {
        val headset =
        val a2dpAttributes =
            AudioDeviceAttributes(
                AudioDeviceAttributes.ROLE_OUTPUT,
                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
                "test_address"
            )
        val bleHeadsetAttributes =
            AudioDeviceAttributes(
            AudioDeviceAttributes(
                AudioDeviceAttributes.ROLE_OUTPUT,
                AudioDeviceAttributes.ROLE_OUTPUT,
                AudioDeviceInfo.TYPE_BLE_HEADSET,
                AudioDeviceInfo.TYPE_BLE_HEADSET,
Loading