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

Commit 54712afc authored by Chelsea Hao's avatar Chelsea Hao Committed by Android (Google) Code Review
Browse files

Merge "Register listener on `onBroadcastMetadataChanged` when audio sharing is...

Merge "Register listener on `onBroadcastMetadataChanged` when audio sharing is triggered instead of after `isAudioSharingOn`." into main
parents 846a61e0 2f4d2f19
Loading
Loading
Loading
Loading
+41 −111
Original line number Diff line number Diff line
@@ -18,20 +18,12 @@ package com.android.systemui.bluetooth.qsdialog

import android.bluetooth.BluetoothLeBroadcast
import android.bluetooth.BluetoothLeBroadcastMetadata
import android.content.ContentResolver
import android.content.applicationContext
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.settingslib.bluetooth.VolumeControlProfile
import com.android.settingslib.volume.shared.AudioSharingLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -46,14 +38,10 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.whenever

@@ -78,7 +66,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
    }

    @Test
    fun testIsAudioSharingOn_flagOff_false() =
    fun isAudioSharingOn_flagOff_false() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
@@ -90,7 +78,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() =
    fun isAudioSharingOn_flagOn_notInAudioSharing_false() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
@@ -103,7 +91,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testIsAudioSharingOn_flagOn_inAudioSharing_true() =
    fun isAudioSharingOn_flagOn_inAudioSharing_true() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
@@ -116,7 +104,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() =
    fun audioSourceStateUpdate_notInAudioSharing_returnEmpty() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
@@ -129,7 +117,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() =
    fun audioSourceStateUpdate_inAudioSharing_returnUnit() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
@@ -144,7 +132,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() =
    fun handleAudioSourceWhenReady_flagOff_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
@@ -157,7 +145,7 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() =
    fun handleAudioSourceWhenReady_noProfile_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
@@ -171,36 +159,41 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

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

                // Verify callback registered for onBroadcastStartedOrStopped
                verify(localBluetoothLeBroadcast).registerServiceCallBack(any(), any())
                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                job.cancel()
            }
        }

    @Test
    fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
    fun handleAudioSourceWhenReady_audioSharingTriggeredButFailed_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(localBluetoothLeBroadcast)
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                // Audio sharing started failed, trigger onBroadcastStartFailed
                whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(false)
                underTest.startAudioSharing()
                runCurrent()
                callbackCaptor.value.onBroadcastStartFailed(0)
                runCurrent()

                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
@@ -209,122 +202,59 @@ class AudioSharingInteractorTest : SysuiTestCase() {
        }

    @Test
    fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
    fun handleAudioSourceWhenReady_audioSharingTriggeredButMetadataNotReady_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(localBluetoothLeBroadcast)
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
                callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
                underTest.startAudioSharing()
                runCurrent()
                // Verify callback registered for onBroadcastMetadataChanged
                verify(localBluetoothLeBroadcast, times(2))
                    .registerServiceCallBack(any(), callbackCaptor.capture())

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

    @Test
    fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                val (broadcast, repository) = setupRepositoryImpl()
                val interactor =
                    object :
                        AudioSharingInteractorImpl(
                            applicationContext,
                            localBluetoothManager,
                            repository,
                            testDispatcher,
                        ) {
                        override suspend fun audioSharingAvailable() = true
                    }
                val job = launch { interactor.handleAudioSourceWhenReady() }
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
                // Verify source is not added
                verify(repository, never()).addSource()
                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                job.cancel()
            }
        }

    @Test
    fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() =
    fun handleAudioSourceWhenReady_audioSharingTriggeredAndMetadataReady_sourceAdded() =
        with(kosmos) {
            testScope.runTest {
                val (broadcast, repository) = setupRepositoryImpl()
                val interactor =
                    object :
                        AudioSharingInteractorImpl(
                            applicationContext,
                            localBluetoothManager,
                            repository,
                            testDispatcher,
                        ) {
                        override suspend fun audioSharingAvailable() = true
                    }
                val job = launch { interactor.handleAudioSourceWhenReady() }
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
                verify(localBluetoothLeBroadcast)
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                // Audio sharing started, trigger onBroadcastStarted
                whenever(broadcast.isEnabled(null)).thenReturn(true)
                whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true)
                underTest.startAudioSharing()
                runCurrent()
                callbackCaptor.value.onBroadcastStarted(0, 0)
                runCurrent()
                // Verify callback registered for onBroadcastMetadataChanged
                verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture())
                verify(localBluetoothLeBroadcast, times(2))
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
                // Trigger onBroadcastMetadataChanged (ready to add source)
                callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
                runCurrent()
                // Verify source added
                verify(repository).addSource()
                job.cancel()
            }
        }

    private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> {
        with(kosmos) {
            val broadcast =
                mock<LocalBluetoothLeBroadcast> {
                    on { isProfileReady } doReturn true
                    on { isEnabled(null) } doReturn false
                }
            val assistant =
                mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true }
            val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true }
            val profileManager =
                mock<LocalBluetoothProfileManager> {
                    on { leAudioBroadcastProfile } doReturn broadcast
                    on { leAudioBroadcastAssistantProfile } doReturn assistant
                    on { volumeControlProfile } doReturn volumeControl
                }
            whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
            whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {})

            val repository =
                AudioSharingRepositoryImpl(
                    localBluetoothManager,
                    com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl(
                        mock<ContentResolver> {},
                        localBluetoothManager,
                        testScope.backgroundScope,
                        testScope.testScheduler,
                        mock<AudioSharingLogger> {},
                    ),
                    testDispatcher,
                )
            return Pair(broadcast, spy(repository))
                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue()
                job.cancel()
            }
        }
}
+20 −7
Original line number Diff line number Diff line
@@ -22,20 +22,25 @@ import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.onBroadcastMetadataChanged
import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped
import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.withContext

/** Holds business logic for the audio sharing state. */
@@ -71,6 +76,7 @@ constructor(
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : AudioSharingInteractor {

    private val audioSharingStartedEvents = Channel<Unit>(Channel.BUFFERED)
    private var previewEnabled: Boolean? = null

    override val isAudioSharingOn: Flow<Boolean> =
@@ -99,12 +105,18 @@ constructor(
        withContext(backgroundDispatcher) {
            if (audioSharingAvailable()) {
                audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                    isAudioSharingOn
                        // Skip the default value, we only care about adding source for newly
                        // started audio sharing session
                        .drop(1)
                        .mapNotNull { audioSharingOn ->
                            if (audioSharingOn) {
                    merge(
                            // Register and start listen to onBroadcastMetadataChanged (means ready
                            // to add source)
                            audioSharingStartedEvents.receiveAsFlow().map { true },
                            // When session is off or failed to start, stop listening to
                            // onBroadcastMetadataChanged as we won't be adding source
                            profile.onBroadcastStartedOrStopped
                                .filterNot { profile.isEnabled(null) }
                                .map { false },
                        )
                        .mapNotNull { shouldListenToMetadata ->
                            if (shouldListenToMetadata) {
                                // onBroadcastMetadataChanged could emit multiple times during one
                                // audio sharing session, we only perform add source on the first
                                // time
@@ -146,6 +158,7 @@ constructor(
        if (!audioSharingAvailable()) {
            return
        }
        audioSharingStartedEvents.trySend(Unit)
        audioSharingRepository.startAudioSharing()
    }