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

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

Merge "Determine Spatial Audio AudioDeviceAttributes by BT profile state" into main

parents ff2b2375 765df2d8
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -79,3 +79,13 @@ flag {
        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 Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.ContentResolver
import android.database.ContentObserver
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.AudioDeviceCategory
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import android.provider.Settings
import androidx.concurrent.futures.DirectExecutor
@@ -85,6 +86,10 @@ interface AudioRepository {
    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean): Boolean

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

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

class AudioRepositoryImpl(
@@ -211,6 +216,13 @@ class AudioRepositoryImpl(
        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 =
        try {
            audioManager.getStreamMinVolume(stream.value)
+13 −0
Original line number 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?) {
        verify(audioManager)
            .addOnCommunicationDeviceChangedListener(
+4 −0
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.systemui.volume.panel.component.spatial

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.testScope
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.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor

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

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.AudioDeviceInfo
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.A2dpProfile
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.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -44,6 +52,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

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

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

                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
    fun connectedDeviceSupports_isAvailable_SpatialAudio() {
        with(kosmos) {
            testScope.runTest {
                spatializerRepository.setIsSpatialAudioAvailable(headset, true)
                spatializerRepository.setIsSpatialAudioAvailable(bleHeadsetAttributes, true)

                val isAvailable by collectLastValue(underTest.isAvailable)

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

                val isAvailable by collectLastValue(underTest.isAvailable)

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

                val isAvailable by collectLastValue(underTest.isAvailable)

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

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