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

Commit ba7ef7b2 authored by chelseahao's avatar chelseahao
Browse files

Listen to profile readiness before registering callbacks for audio sharing.

Profiles won't be ready if BT is off or profile connection is slow when launching the BT dialog.

Test: atest
Bug: 382397280
Flag: com.android.settingslib.flags.enable_le_audio_sharing
Change-Id: Ibfa84c023011943f7f8833e3dd0747f570655aca
parent c10e66c9
Loading
Loading
Loading
Loading
+37 −0
Original line number Original line Diff line number Diff line
@@ -101,12 +101,27 @@ class AudioSharingInteractorTest : SysuiTestCase() {
            }
            }
        }
        }


    @Test
    fun audioSourceStateUpdate_profileNotReady_returnEmpty() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(false)
                val value by collectLastValue(underTest.audioSourceStateUpdate)
                runCurrent()

                assertThat(value).isNull()
            }
        }

    @Test
    @Test
    fun audioSourceStateUpdate_notInAudioSharing_returnEmpty() =
    fun audioSourceStateUpdate_notInAudioSharing_returnEmpty() =
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val value by collectLastValue(underTest.audioSourceStateUpdate)
                val value by collectLastValue(underTest.audioSourceStateUpdate)
                runCurrent()
                runCurrent()


@@ -120,6 +135,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
            testScope.runTest {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val value by collectLastValue(underTest.audioSourceStateUpdate)
                val value by collectLastValue(underTest.audioSourceStateUpdate)
                runCurrent()
                runCurrent()
                bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate()
                bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate()
@@ -158,6 +174,23 @@ class AudioSharingInteractorTest : SysuiTestCase() {
            }
            }
        }
        }


    @Test
    fun handleAudioSourceWhenReady_profileNotReady_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(false)
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()

                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                job.cancel()
            }
        }

    @Test
    @Test
    fun handleAudioSourceWhenReady_hasProfileButAudioSharingNeverTriggered_sourceNotAdded() =
    fun handleAudioSourceWhenReady_hasProfileButAudioSharingNeverTriggered_sourceNotAdded() =
        with(kosmos) {
        with(kosmos) {
@@ -166,6 +199,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                    localBluetoothLeBroadcast
                )
                )
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val job = launch { underTest.handleAudioSourceWhenReady() }
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                runCurrent()


@@ -184,6 +218,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                    localBluetoothLeBroadcast
                )
                )
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val job = launch { underTest.handleAudioSourceWhenReady() }
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                // Verify callback registered for onBroadcastStartedOrStopped
@@ -209,6 +244,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                    localBluetoothLeBroadcast
                )
                )
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val job = launch { underTest.handleAudioSourceWhenReady() }
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                // Verify callback registered for onBroadcastStartedOrStopped
@@ -234,6 +270,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                    localBluetoothLeBroadcast
                )
                )
                bluetoothTileDialogAudioSharingRepository.setIsAudioSharingProfilesReady(true)
                val job = launch { underTest.handleAudioSourceWhenReady() }
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                // Verify callback registered for onBroadcastStartedOrStopped
+38 −3
Original line number Original line Diff line number Diff line
@@ -24,10 +24,12 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.testKosmos
import com.android.systemui.volume.data.repository.audioSharingRepository
import com.android.systemui.volume.data.repository.audioSharingRepository
import com.google.common.truth.Truth.assertThat
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
@@ -58,12 +60,14 @@ class AudioSharingRepositoryTest : SysuiTestCase() {


    @Before
    @Before
    fun setUp() {
    fun setUp() {
        whenever(kosmos.localBluetoothManager.profileManager).thenReturn(profileManager)
        underTest =
        underTest =
            AudioSharingRepositoryImpl(
            AudioSharingRepositoryImpl(
                kosmos.localBluetoothManager,
                kosmos.localBluetoothManager,
                kosmos.audioSharingRepository,
                kosmos.audioSharingRepository,
                kosmos.bluetoothTileDialogLogger,
                kosmos.bluetoothTileDialogLogger,
                kosmos.testDispatcher,
                kosmos.testDispatcher,
                kosmos.testScope.backgroundScope,
            )
            )
    }
    }


@@ -91,7 +95,6 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
    fun testStartAudioSharing() =
    fun testStartAudioSharing() =
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                audioSharingRepository.setAudioSharingAvailable(true)
                audioSharingRepository.setAudioSharingAvailable(true)
                underTest.startAudioSharing()
                underTest.startAudioSharing()
@@ -117,7 +120,6 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
    fun testStopAudioSharing() =
    fun testStopAudioSharing() =
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                audioSharingRepository.setAudioSharingAvailable(true)
                audioSharingRepository.setAudioSharingAvailable(true)
                underTest.stopAudioSharing()
                underTest.stopAudioSharing()
@@ -155,7 +157,6 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
    fun testAddSource_noMetadata_doesNothing() =
    fun testAddSource_noMetadata_doesNothing() =
        with(kosmos) {
        with(kosmos) {
            testScope.runTest {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                audioSharingRepository.setAudioSharingAvailable(true)
                audioSharingRepository.setAudioSharingAvailable(true)
                whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
                whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
@@ -212,4 +213,38 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
                    .logAudioSharingRequest(AudioSharingRequest.ADD_SOURCE)
                    .logAudioSharingRequest(AudioSharingRequest.ADD_SOURCE)
            }
            }
        }
        }

    @Test
    fun testIsAudioSharingProfilesReady_notReady() =
        with(kosmos) {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                whenever(profileManager.leAudioBroadcastAssistantProfile)
                    .thenReturn(leAudioBroadcastAssistant)
                whenever(leAudioBroadcastProfile.isProfileReady).thenReturn(false)
                whenever(leAudioBroadcastAssistant.isProfileReady).thenReturn(false)
                val value by collectLastValue(underTest.isAudioSharingProfilesReady)
                runCurrent()

                assertThat(value).isFalse()
            }
        }

    @Test
    fun testIsAudioSharingProfilesReady_ready() =
        with(kosmos) {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                whenever(profileManager.leAudioBroadcastAssistantProfile)
                    .thenReturn(leAudioBroadcastAssistant)
                whenever(leAudioBroadcastProfile.isProfileReady).thenReturn(true)
                whenever(leAudioBroadcastAssistant.isProfileReady).thenReturn(true)
                val value by collectLastValue(underTest.isAudioSharingProfilesReady)
                runCurrent()

                assertThat(value).isTrue()
            }
        }
}
}
+8 −5
Original line number Original line Diff line number Diff line
@@ -31,7 +31,9 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flow
@@ -109,10 +111,11 @@ constructor(
    override suspend fun handleAudioSourceWhenReady() {
    override suspend fun handleAudioSourceWhenReady() {
        withContext(backgroundDispatcher) {
        withContext(backgroundDispatcher) {
            if (audioSharingAvailable()) {
            if (audioSharingAvailable()) {
                audioSharingRepository.isAudioSharingProfilesReady.filter { it }.first()
                audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                    merge(
                    merge(
                            // Register and start listen to onBroadcastMetadataChanged (means ready
                            // Register and start listen to onBroadcastMetadataChanged
                            // to add source)
                            // (means ready to add source)
                            audioSharingStartedEvents.receiveAsFlow().map { true },
                            audioSharingStartedEvents.receiveAsFlow().map { true },
                            // When session is off or failed to start, stop listening to
                            // When session is off or failed to start, stop listening to
                            // onBroadcastMetadataChanged as we won't be adding source
                            // onBroadcastMetadataChanged as we won't be adding source
@@ -122,9 +125,9 @@ constructor(
                        )
                        )
                        .mapNotNull { shouldListenToMetadata ->
                        .mapNotNull { shouldListenToMetadata ->
                            if (shouldListenToMetadata) {
                            if (shouldListenToMetadata) {
                                // onBroadcastMetadataChanged could emit multiple times during one
                                // onBroadcastMetadataChanged could emit multiple times
                                // audio sharing session, we only perform add source on the first
                                // during one audio sharing session, we only perform add
                                // time
                                // source on the first time
                                profile.onBroadcastMetadataChanged.firstOrNull()
                                profile.onBroadcastMetadataChanged.firstOrNull()
                            } else {
                            } else {
                                null
                                null
+40 −3
Original line number Original line Diff line number Diff line
@@ -20,18 +20,29 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.onServiceStateChanged
import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Background
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withContext


interface AudioSharingRepository {
interface AudioSharingRepository {
    val isAudioSharingProfilesReady: StateFlow<Boolean>

    val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
    val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?


    val audioSourceStateUpdate: Flow<Unit>
    val audioSourceStateUpdate: Flow<Unit>
@@ -55,16 +66,40 @@ class AudioSharingRepositoryImpl(
    private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
    private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
    private val logger: BluetoothTileDialogLogger,
    private val logger: BluetoothTileDialogLogger,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @Application private val coroutineScope: CoroutineScope,
) : AudioSharingRepository {
) : AudioSharingRepository {


    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
        get() = localBluetoothManager.profileManager?.leAudioBroadcastProfile
        get() = localBluetoothManager.profileManager.leAudioBroadcastProfile


    private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant?
    private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant?
        get() = localBluetoothManager.profileManager?.leAudioBroadcastAssistantProfile
        get() = localBluetoothManager.profileManager.leAudioBroadcastAssistantProfile

    override val isAudioSharingProfilesReady: StateFlow<Boolean> =
        localBluetoothManager.profileManager.onServiceStateChanged
            .map {
                leAudioBroadcastProfile?.isProfileReady == true &&
                    leAudioBroadcastAssistantProfile?.isProfileReady == true
            }
            .onStart {
                emit(
                    leAudioBroadcastProfile?.isProfileReady == true &&
                        leAudioBroadcastAssistantProfile?.isProfileReady == true
                )
            }
            .flowOn(backgroundDispatcher)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false)


    override val audioSourceStateUpdate: Flow<Unit> =
    override val audioSourceStateUpdate: Flow<Unit> =
        isAudioSharingProfilesReady
            .flatMapLatest {
                if (it) {
                    leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow()
                    leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow()
                } else {
                    emptyFlow()
                }
            }
            .flowOn(backgroundDispatcher)


    override val inAudioSharing: StateFlow<Boolean> =
    override val inAudioSharing: StateFlow<Boolean> =
        settingsLibAudioSharingRepository.inAudioSharing
        settingsLibAudioSharingRepository.inAudioSharing
@@ -124,6 +159,8 @@ class AudioSharingRepositoryImpl(


@SysUISingleton
@SysUISingleton
class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
    override val isAudioSharingProfilesReady: StateFlow<Boolean> = MutableStateFlow(false)

    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null
    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null


    override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
    override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+4 −0
Original line number Original line Diff line number Diff line
@@ -38,11 +38,13 @@ import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory
import com.android.systemui.bluetooth.qsdialog.DeviceItemType
import com.android.systemui.bluetooth.qsdialog.DeviceItemType
import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory
import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Background
import dagger.Lazy
import dagger.Lazy
import dagger.Module
import dagger.Module
import dagger.Provides
import dagger.Provides
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope


/** Dagger module for audio sharing code for BT QS dialog */
/** Dagger module for audio sharing code for BT QS dialog */
@Module
@Module
@@ -56,6 +58,7 @@ interface AudioSharingModule {
            settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
            settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
            logger: BluetoothTileDialogLogger,
            logger: BluetoothTileDialogLogger,
            @Background backgroundDispatcher: CoroutineDispatcher,
            @Background backgroundDispatcher: CoroutineDispatcher,
            @Application coroutineScope: CoroutineScope,
        ): AudioSharingRepository =
        ): AudioSharingRepository =
            if (
            if (
                (Flags.enableLeAudioSharing() || Flags.audioSharingDeveloperOption()) &&
                (Flags.enableLeAudioSharing() || Flags.audioSharingDeveloperOption()) &&
@@ -66,6 +69,7 @@ interface AudioSharingModule {
                    settingsLibAudioSharingRepository,
                    settingsLibAudioSharingRepository,
                    logger,
                    logger,
                    backgroundDispatcher,
                    backgroundDispatcher,
                    coroutineScope,
                )
                )
            } else {
            } else {
                AudioSharingRepositoryEmptyImpl()
                AudioSharingRepositoryEmptyImpl()
Loading