diff options
author | 2025-01-17 18:39:42 +0800 | |
---|---|---|
committer | 2025-01-28 19:34:51 -0800 | |
commit | 2f4d2f19c9b5833f1c5770a076cecd35931c39f2 (patch) | |
tree | 7a0ec48f126d1aaf9eaca96aea77c58e89759442 | |
parent | ddf806d91c4685fac341bde01aa83b953687c89a (diff) |
Register listener on `onBroadcastMetadataChanged` when audio sharing is triggered instead of after `isAudioSharingOn`.
There are two reasons for this change:
1) there is a chance that `onBroadcastMetadataChanged` callback is missed if it's registered after audio sharing is turned on, as it's fires right after `onBroadcastStarted` (`isAudioSharingOn` emits true)
2) this will also fix a race condition where the settings app triggered `onBroadcastMetadataChanged` before the bluetooth dialog is fully closed. This could happen when user clicks on the "Share Audio" button on the dialog (which leads them to settings and starts audio sharing automatically, in this case, both settings and systemui would perform addSource).
Test: atest
Bug: b/381927583 b/390343739
Flag: com.android.settingslib.flags.enable_le_audio_sharing
Change-Id: I6deed1046ec7d4c5543306a20558c29d68a1e2d7
2 files changed, 61 insertions, 118 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index 4c329dcf2f2b..cebd05d92537 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -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() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() 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)) - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index d82311f6ca7c..832afb1799b1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -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() } |