summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt39
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt85
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt23
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt169
-rw-r--r--packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt324
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt24
7 files changed, 647 insertions, 28 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt
new file mode 100644
index 000000000000..2eaa804152e4
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth
+
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** [Flow] for [BluetoothCallback] device profile connection state change events */
+val BluetoothEventManager.onProfileConnectionStateChanged: Flow<ProfileConnectionState>
+ get() = callbackFlow {
+ val callback =
+ object : BluetoothCallback {
+ override fun onProfileConnectionStateChanged(
+ cachedDevice: CachedBluetoothDevice,
+ @BluetoothCallback.ConnectionState state: Int,
+ bluetoothProfile: Int
+ ) {
+ launch { send(ProfileConnectionState(cachedDevice, state, bluetoothProfile)) }
+ }
+ }
+ registerCallback(callback)
+ awaitClose { unregisterCallback(callback) }
+ }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
new file mode 100644
index 000000000000..91a99aed6db5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeBroadcastAssistant
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.bluetooth.BluetoothLeBroadcastReceiveState
+import com.android.internal.util.ConcurrentUtils
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** [Flow] for [BluetoothLeBroadcastAssistant.Callback] source connected/removed events */
+val LocalBluetoothLeBroadcastAssistant.onSourceConnectedOrRemoved: Flow<Unit>
+ get() = callbackFlow {
+ val callback =
+ object : BluetoothLeBroadcastAssistant.Callback {
+ override fun onReceiveStateChanged(
+ sink: BluetoothDevice,
+ sourceId: Int,
+ state: BluetoothLeBroadcastReceiveState
+ ) {
+ if (BluetoothUtils.isConnected(state)) {
+ launch { send(Unit) }
+ }
+ }
+
+ override fun onSourceRemoved(sink: BluetoothDevice, sourceId: Int, reason: Int) {
+ launch { send(Unit) }
+ }
+
+ override fun onSearchStarted(reason: Int) {}
+
+ override fun onSearchStartFailed(reason: Int) {}
+
+ override fun onSearchStopped(reason: Int) {}
+
+ override fun onSearchStopFailed(reason: Int) {}
+
+ override fun onSourceFound(source: BluetoothLeBroadcastMetadata) {}
+
+ override fun onSourceAdded(sink: BluetoothDevice, sourceId: Int, reason: Int) {}
+
+ override fun onSourceAddFailed(
+ sink: BluetoothDevice,
+ source: BluetoothLeBroadcastMetadata,
+ reason: Int
+ ) {}
+
+ override fun onSourceModified(sink: BluetoothDevice, sourceId: Int, reason: Int) {}
+
+ override fun onSourceModifyFailed(
+ sink: BluetoothDevice,
+ sourceId: Int,
+ reason: Int
+ ) {}
+
+ override fun onSourceRemoveFailed(
+ sink: BluetoothDevice,
+ sourceId: Int,
+ reason: Int
+ ) {}
+ }
+ registerServiceCallBack(
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ callback,
+ )
+ awaitClose { unregisterServiceCallBack(callback) }
+ }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt
new file mode 100644
index 000000000000..45aaa66e7f95
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth
+
+data class ProfileConnectionState(
+ val cachedDevice: CachedBluetoothDevice,
+ @BluetoothCallback.ConnectionState val state: Int,
+ val bluetoothProfile: Int,
+) \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index 9dbf23eba7a5..eb33a7a10524 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -16,33 +16,84 @@
package com.android.settingslib.volume.data.repository
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothCsipSetCoordinator
+import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothLeBroadcast
import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.BluetoothVolumeControl
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings
+import androidx.annotation.IntRange
import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onProfileConnectionStateChanged
+import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
import com.android.settingslib.flags.Flags
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+typealias GroupIdToVolumes = Map<Int, Int>
/** Provides audio sharing functionality. */
interface AudioSharingRepository {
/** Whether the device is in audio sharing. */
val inAudioSharing: Flow<Boolean>
+
+ /** The secondary headset groupId in audio sharing. */
+ val secondaryGroupId: StateFlow<Int>
+
+ /** The headset groupId to volume map during audio sharing. */
+ val volumeMap: StateFlow<GroupIdToVolumes>
+
+ /** Set the volume of secondary headset during audio sharing. */
+ suspend fun setSecondaryVolume(
+ @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+ volume: Int
+ )
+
+ companion object {
+ const val AUDIO_SHARING_VOLUME_MIN = 0
+ const val AUDIO_SHARING_VOLUME_MAX = 255
+ }
}
+@OptIn(ExperimentalCoroutinesApi::class)
class AudioSharingRepositoryImpl(
- private val localBluetoothManager: LocalBluetoothManager?,
- backgroundCoroutineContext: CoroutineContext,
+ private val context: Context,
+ private val contentResolver: ContentResolver,
+ private val btManager: LocalBluetoothManager?,
+ private val coroutineScope: CoroutineScope,
+ private val backgroundCoroutineContext: CoroutineContext,
) : AudioSharingRepository {
override val inAudioSharing: Flow<Boolean> =
if (Flags.enableLeAudioSharing()) {
- localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast ->
+ btManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast ->
callbackFlow {
val listener =
object : BluetoothLeBroadcast.Callback {
@@ -92,9 +143,117 @@ class AudioSharingRepositoryImpl(
flowOf(false)
}
+ private val primaryChange: Flow<Unit> = callbackFlow {
+ val callback =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ launch { send(Unit) }
+ }
+ }
+ contentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()),
+ false,
+ callback)
+ awaitClose { contentResolver.unregisterContentObserver(callback) }
+ }
+
+ override val secondaryGroupId: StateFlow<Int> =
+ if (Flags.volumeDialogAudioSharingFix()) {
+ merge(
+ btManager
+ ?.profileManager
+ ?.leAudioBroadcastAssistantProfile
+ ?.onSourceConnectedOrRemoved
+ ?.map { getSecondaryGroupId() } ?: emptyFlow(),
+ btManager
+ ?.eventManager
+ ?.onProfileConnectionStateChanged
+ ?.filter { profileConnection ->
+ profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED &&
+ profileConnection.bluetoothProfile ==
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+ }
+ ?.map { getSecondaryGroupId() } ?: emptyFlow(),
+ primaryChange.map { getSecondaryGroupId() })
+ .onStart { emit(getSecondaryGroupId()) }
+ .distinctUntilChanged()
+ .flowOn(backgroundCoroutineContext)
+ } else {
+ emptyFlow()
+ }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId())
+
+ override val volumeMap: StateFlow<GroupIdToVolumes> =
+ if (Flags.volumeDialogAudioSharingFix()) {
+ btManager?.profileManager?.volumeControlProfile?.let { volumeControl ->
+ inAudioSharing.flatMapLatest { isSharing ->
+ if (isSharing) {
+ callbackFlow {
+ val callback =
+ object : BluetoothVolumeControl.Callback {
+ override fun onDeviceVolumeChanged(
+ device: BluetoothDevice,
+ @IntRange(
+ from = AUDIO_SHARING_VOLUME_MIN.toLong(),
+ to = AUDIO_SHARING_VOLUME_MAX.toLong())
+ volume: Int
+ ) {
+ launch { send(Pair(device, volume)) }
+ }
+ }
+ // Once registered, we will receive the initial volume of all
+ // connected BT devices on VolumeControlProfile via callbacks
+ volumeControl.registerCallback(
+ ConcurrentUtils.DIRECT_EXECUTOR, callback)
+ awaitClose { volumeControl.unregisterCallback(callback) }
+ }
+ .runningFold(emptyMap<Int, Int>()) { acc, value ->
+ val groupId =
+ BluetoothUtils.getGroupId(
+ btManager.cachedDeviceManager?.findDevice(value.first))
+ if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ acc + Pair(groupId, value.second)
+ } else {
+ acc
+ }
+ }
+ .distinctUntilChanged()
+ .flowOn(backgroundCoroutineContext)
+ } else {
+ emptyFlow()
+ }
+ }
+ } ?: emptyFlow()
+ } else {
+ emptyFlow()
+ }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap())
+
+ override suspend fun setSecondaryVolume(
+ @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+ volume: Int
+ ) {
+ withContext(backgroundCoroutineContext) {
+ if (Flags.volumeDialogAudioSharingFix()) {
+ btManager?.profileManager?.volumeControlProfile?.let {
+ // Find secondary headset and set volume.
+ val cachedDevice =
+ BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager)
+ if (cachedDevice != null) {
+ it.setDeviceVolume(cachedDevice.device, volume, /* isGroupOp= */ true)
+ }
+ }
+ }
+ }
+ }
+
private fun isBroadcasting(): Boolean {
return Flags.enableLeAudioSharing() &&
- (localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null)
- ?: false)
+ (btManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null) ?: false)
+ }
+
+ private fun getSecondaryGroupId(): Int {
+ return BluetoothUtils.getGroupId(
+ BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager))
}
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
index 1c80ef4fe4d4..000664dd1552 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
@@ -16,15 +16,33 @@
package com.android.settingslib.volume.data.repository
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothLeBroadcast
+import android.bluetooth.BluetoothLeBroadcastAssistant
+import android.bluetooth.BluetoothLeBroadcastReceiveState
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.BluetoothVolumeControl
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothEventManager
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.bluetooth.VolumeControlProfile
import com.android.settingslib.flags.Flags
import com.google.common.truth.Truth
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -39,6 +57,9 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.never
@@ -52,27 +73,76 @@ import org.mockito.junit.MockitoRule
@RunWith(AndroidJUnit4::class)
class AudioSharingRepositoryTest {
@get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
+
@get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
- @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
- @Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager
- @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+ @Mock private lateinit var btManager: LocalBluetoothManager
+
+ @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+
+ @Mock private lateinit var broadcast: LocalBluetoothLeBroadcast
+
+ @Mock private lateinit var assistant: LocalBluetoothLeBroadcastAssistant
+
+ @Mock private lateinit var volumeControl: VolumeControlProfile
+
+ @Mock private lateinit var eventManager: BluetoothEventManager
+
+ @Mock private lateinit var deviceManager: CachedBluetoothDeviceManager
+
+ @Mock private lateinit var device1: BluetoothDevice
+
+ @Mock private lateinit var device2: BluetoothDevice
+
+ @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
+
+ @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
+
+ @Mock private lateinit var receiveState: BluetoothLeBroadcastReceiveState
+
+ @Mock private lateinit var contentResolver: ContentResolver
@Captor
- private lateinit var leBroadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
- private val testScope = TestScope()
+ private lateinit var broadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
+
+ @Captor
+ private lateinit var assistantCallbackCaptor:
+ ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback>
+
+ @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback>
+
+ @Captor private lateinit var contentObserverCaptor: ArgumentCaptor<ContentObserver>
+ @Captor
+ private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback>
+
+ private val testScope = TestScope()
+ private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var underTest: AudioSharingRepository
@Before
fun setup() {
- `when`(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
- `when`(localBluetoothProfileManager.leAudioBroadcastProfile)
- .thenReturn(localBluetoothLeBroadcast)
- `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true)
+ `when`(btManager.profileManager).thenReturn(profileManager)
+ `when`(profileManager.leAudioBroadcastProfile).thenReturn(broadcast)
+ `when`(profileManager.leAudioBroadcastAssistantProfile).thenReturn(assistant)
+ `when`(profileManager.volumeControlProfile).thenReturn(volumeControl)
+ `when`(btManager.eventManager).thenReturn(eventManager)
+ `when`(btManager.cachedDeviceManager).thenReturn(deviceManager)
+ `when`(broadcast.isEnabled(null)).thenReturn(true)
+ `when`(cachedDevice1.groupId).thenReturn(TEST_GROUP_ID1)
+ `when`(cachedDevice1.device).thenReturn(device1)
+ `when`(deviceManager.findDevice(device1)).thenReturn(cachedDevice1)
+ `when`(cachedDevice2.groupId).thenReturn(TEST_GROUP_ID2)
+ `when`(cachedDevice2.device).thenReturn(device2)
+ `when`(deviceManager.findDevice(device2)).thenReturn(cachedDevice2)
+ `when`(receiveState.bisSyncState).thenReturn(arrayListOf(TEST_RECEIVE_STATE_CONTENT))
+ `when`(assistant.getAllSources(any())).thenReturn(listOf(receiveState))
underTest =
AudioSharingRepositoryImpl(
- localBluetoothManager,
+ context,
+ contentResolver,
+ btManager,
+ testScope.backgroundScope,
testScope.testScheduler,
)
}
@@ -84,9 +154,9 @@ class AudioSharingRepositoryTest {
val states = mutableListOf<Boolean?>()
underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope)
runCurrent()
- triggerAudioSharingStateChange(false)
+ triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
runCurrent()
- triggerAudioSharingStateChange(true)
+ triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted)
runCurrent()
Truth.assertThat(states).containsExactly(true, false, true)
@@ -102,19 +172,229 @@ class AudioSharingRepositoryTest {
runCurrent()
Truth.assertThat(states).containsExactly(false)
- verify(localBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any())
- verify(localBluetoothLeBroadcast, never()).isEnabled(any())
+ verify(broadcast, never()).registerServiceCallBack(any(), any())
+ verify(broadcast, never()).isEnabled(any())
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun secondaryGroupIdChange_emitValues() {
+ testScope.runTest {
+ val groupIds = mutableListOf<Int?>()
+ underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+ triggerSourceAdded()
+ runCurrent()
+ triggerContentObserverChange()
+ runCurrent()
+ triggerSourceRemoved()
+ runCurrent()
+ triggerSourceAdded()
+ runCurrent()
+ triggerProfileConnectionChange(
+ BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)
+ runCurrent()
+ triggerProfileConnectionChange(
+ BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO)
+ runCurrent()
+ triggerProfileConnectionChange(
+ BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)
+ runCurrent()
+
+ Truth.assertThat(groupIds)
+ .containsExactly(
+ TEST_GROUP_ID_INVALID,
+ TEST_GROUP_ID2,
+ TEST_GROUP_ID1,
+ TEST_GROUP_ID_INVALID,
+ TEST_GROUP_ID2,
+ TEST_GROUP_ID_INVALID)
}
}
- private fun triggerAudioSharingStateChange(inAudioSharing: Boolean) {
- verify(localBluetoothLeBroadcast)
- .registerServiceCallBack(any(), leBroadcastCallbackCaptor.capture())
- `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(inAudioSharing)
- if (inAudioSharing) {
- leBroadcastCallbackCaptor.value.onBroadcastStarted(0, 0)
- } else {
- leBroadcastCallbackCaptor.value.onBroadcastStopped(0, 0)
+ @Test
+ @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun secondaryGroupIdChange_audioSharingFlagOff_returnFalse() {
+ testScope.runTest {
+ val groupIds = mutableListOf<Int?>()
+ underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+
+ Truth.assertThat(groupIds).containsExactly(TEST_GROUP_ID_INVALID)
+ verify(assistant, never()).registerServiceCallBack(any(), any())
+ verify(eventManager, never()).registerCallback(any())
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun volumeMapChange_emitValues() {
+ testScope.runTest {
+ val volumeMaps = mutableListOf<GroupIdToVolumes?>()
+ underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+ triggerVolumeMapChange(Pair(device1, TEST_VOLUME1))
+ runCurrent()
+ triggerVolumeMapChange(Pair(device1, TEST_VOLUME2))
+ runCurrent()
+ triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
+ runCurrent()
+ verify(volumeControl).unregisterCallback(any())
+ runCurrent()
+
+ Truth.assertThat(volumeMaps)
+ .containsExactly(
+ emptyMap<Int, Int>(),
+ mapOf(TEST_GROUP_ID1 to TEST_VOLUME1),
+ mapOf(TEST_GROUP_ID1 to TEST_VOLUME2))
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun volumeMapChange_audioSharingFlagOff_returnFalse() {
+ testScope.runTest {
+ val volumeMaps = mutableListOf<GroupIdToVolumes?>()
+ underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+
+ Truth.assertThat(volumeMaps).isEmpty()
+ verify(broadcast, never()).registerServiceCallBack(any(), any())
+ verify(volumeControl, never()).registerCallback(any(), any())
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun setSecondaryVolume_setValue() {
+ testScope.runTest {
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID2)
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+ underTest.setSecondaryVolume(TEST_VOLUME1)
+
+ runCurrent()
+ verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+ fun setSecondaryVolume_audioSharingFlagOff_doNothing() {
+ testScope.runTest {
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID2)
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+ underTest.setSecondaryVolume(TEST_VOLUME1)
+
+ runCurrent()
+ verify(volumeControl, never()).setDeviceVolume(any(), anyInt(), anyBoolean())
+ }
+ }
+
+ private fun triggerAudioSharingStateChange(
+ type: TriggerType,
+ broadcastAction: BluetoothLeBroadcast.Callback.() -> Unit
+ ) {
+ verify(broadcast).registerServiceCallBack(any(), broadcastCallbackCaptor.capture())
+ when (type) {
+ TriggerType.BROADCAST_START -> {
+ `when`(broadcast.isEnabled(null)).thenReturn(true)
+ broadcastCallbackCaptor.value.broadcastAction()
+ }
+ TriggerType.BROADCAST_STOP -> {
+ `when`(broadcast.isEnabled(null)).thenReturn(false)
+ broadcastCallbackCaptor.value.broadcastAction()
+ }
+ }
+ }
+
+ private fun triggerSourceAdded() {
+ verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID1)
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+ assistantCallbackCaptor.value.sourceAdded(device1, receiveState)
+ }
+
+ private fun triggerSourceRemoved() {
+ verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID1)
+ assistantCallbackCaptor.value.sourceRemoved(device2)
+ }
+
+ private fun triggerProfileConnectionChange(state: Int, profile: Int) {
+ verify(eventManager).registerCallback(btCallbackCaptor.capture())
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID1)
+ btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile)
+ }
+
+ private fun triggerContentObserverChange() {
+ verify(contentResolver)
+ .registerContentObserver(
+ eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())),
+ eq(false),
+ contentObserverCaptor.capture())
+ `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+ Settings.Secure.putInt(
+ context.contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID2)
+ contentObserverCaptor.value.primaryChanged()
+ }
+
+ private fun triggerVolumeMapChange(change: Pair<BluetoothDevice, Int>) {
+ verify(volumeControl).registerCallback(any(), volumeCallbackCaptor.capture())
+ volumeCallbackCaptor.value.onDeviceVolumeChanged(change.first, change.second)
+ }
+
+ private enum class TriggerType {
+ BROADCAST_START,
+ BROADCAST_STOP
+ }
+
+ private companion object {
+ const val TEST_GROUP_ID_INVALID = -1
+ const val TEST_GROUP_ID1 = 1
+ const val TEST_GROUP_ID2 = 2
+ const val TEST_SOURCE_ID = 1
+ const val TEST_BROADCAST_ID = 1
+ const val TEST_REASON = 1
+ const val TEST_RECEIVE_STATE_CONTENT = 1L
+ const val TEST_VOLUME1 = 10
+ const val TEST_VOLUME2 = 20
+
+ val broadcastStarted: BluetoothLeBroadcast.Callback.() -> Unit = {
+ onBroadcastStarted(TEST_REASON, TEST_BROADCAST_ID)
+ }
+ val broadcastStopped: BluetoothLeBroadcast.Callback.() -> Unit = {
+ onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID)
}
+ val sourceAdded:
+ BluetoothLeBroadcastAssistant.Callback.(
+ sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState) -> Unit =
+ { sink, state ->
+ onReceiveStateChanged(sink, TEST_SOURCE_ID, state)
+ }
+ val sourceRemoved: BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit =
+ { sink ->
+ onSourceRemoved(sink, TEST_SOURCE_ID, TEST_REASON)
+ }
+ val primaryChanged: ContentObserver.() -> Unit = { onChange(false) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index de8b9b1e7e3c..eb2f71a1cd7d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -73,10 +73,19 @@ interface AudioModule {
@Provides
@SysUISingleton
fun provideAudioSharingRepository(
+ @Application context: Context,
+ contentResolver: ContentResolver,
localBluetoothManager: LocalBluetoothManager?,
+ @Application coroutineScope: CoroutineScope,
@Background coroutineContext: CoroutineContext,
): AudioSharingRepository =
- AudioSharingRepositoryImpl(localBluetoothManager, coroutineContext)
+ AudioSharingRepositoryImpl(
+ context,
+ contentResolver,
+ localBluetoothManager,
+ coroutineScope,
+ coroutineContext
+ )
@Provides
@SysUISingleton
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index 327e1b5df2ac..d391750a2612 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -16,16 +16,40 @@
package com.android.systemui.volume.data.repository
+import androidx.annotation.IntRange
import com.android.settingslib.volume.data.repository.AudioSharingRepository
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
+import com.android.settingslib.volume.data.repository.GroupIdToVolumes
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
class FakeAudioSharingRepository : AudioSharingRepository {
private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ private val mutableSecondaryGroupId: MutableStateFlow<Int> =
+ MutableStateFlow(TEST_GROUP_ID_INVALID)
+ private val mutableVolumeMap: MutableStateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
override val inAudioSharing: Flow<Boolean> = mutableInAudioSharing
+ override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
+ override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
+
+ override suspend fun setSecondaryVolume(volume: Int) {}
fun setInAudioSharing(state: Boolean) {
mutableInAudioSharing.value = state
}
+
+ fun setSecondaryGroupId(groupId: Int) {
+ mutableSecondaryGroupId.value = groupId
+ }
+
+ fun setVolumeMap(volumeMap: GroupIdToVolumes) {
+ mutableVolumeMap.value = volumeMap
+ }
+
+ private companion object {
+ const val TEST_GROUP_ID_INVALID = -1
+ }
}