summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author chelseahao <chelseahao@google.com> 2025-01-17 18:39:42 +0800
committer Chelsea Hao <chelseahao@google.com> 2025-01-28 19:34:51 -0800
commit2f4d2f19c9b5833f1c5770a076cecd35931c39f2 (patch)
tree7a0ec48f126d1aaf9eaca96aea77c58e89759442
parentddf806d91c4685fac341bde01aa83b953687c89a (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
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt152
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt27
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()
}