diff options
20 files changed, 602 insertions, 49 deletions
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 4c4ce2a61851..01bf0c8335d1 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 @@ -27,6 +27,7 @@ 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.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.bluetooth.onProfileConnectionStateChanged @@ -71,6 +72,12 @@ interface AudioSharingRepository { /** The secondary headset groupId in audio sharing. */ val secondaryGroupId: StateFlow<Int> + /** Primary audio sharing device. */ + val primaryDevice: StateFlow<CachedBluetoothDevice?> + + /** Secondary audio sharing device. */ + val secondaryDevice: StateFlow<CachedBluetoothDevice?> + /** The headset groupId to volume map during audio sharing. */ val volumeMap: StateFlow<GroupIdToVolumes> @@ -144,12 +151,31 @@ class AudioSharingRepositoryImpl( ) override val secondaryGroupId: StateFlow<Int> = - merge( + secondaryDevice + .map { BluetoothUtils.getGroupId(it) } + .onEach { logger.onSecondaryGroupIdChanged(it) } + .flowOn(backgroundCoroutineContext) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + BluetoothCsipSetCoordinator.GROUP_ID_INVALID + ) + + override val primaryDevice: StateFlow<CachedBluetoothDevice?> + get() = primaryGroupId.map { getCachedDeviceFromGroupId(it) } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + null + ) + + override val secondaryDevice: StateFlow<CachedBluetoothDevice?> + get() = merge( isAudioSharingProfilesReady.flatMapLatest { ready -> if (ready) { btManager.profileManager.leAudioBroadcastAssistantProfile .onSourceConnectedOrRemoved - .map { getSecondaryGroupId() } + .map { getSecondaryDevice() } } else { emptyFlow() } @@ -160,15 +186,14 @@ class AudioSharingRepositoryImpl( profileConnection.bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT } - .map { getSecondaryGroupId() }, - primaryGroupId.map { getSecondaryGroupId() }) - .onStart { emit(getSecondaryGroupId()) } - .onEach { logger.onSecondaryGroupIdChanged(it) } + .map { getSecondaryDevice() }, + primaryGroupId.map { getSecondaryDevice() }) + .onStart { emit(getSecondaryDevice()) } .flowOn(backgroundCoroutineContext) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), - BluetoothCsipSetCoordinator.GROUP_ID_INVALID + null ) override val volumeMap: StateFlow<GroupIdToVolumes> = @@ -257,10 +282,24 @@ class AudioSharingRepositoryImpl( private fun isBroadcasting(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isEnabled(null) ?: false - private fun getSecondaryGroupId(): Int = - BluetoothUtils.getGroupId( - BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) - ) + private fun getSecondaryDevice(): CachedBluetoothDevice? = + BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) + + private suspend fun getCachedDeviceFromGroupId(groupId: Int): CachedBluetoothDevice? = + withContext(backgroundCoroutineContext) { + btManager + .profileManager + ?.leAudioBroadcastAssistantProfile + ?.allConnectedDevices + ?.firstNotNullOfOrNull { device -> + val cachedDevice = btManager.cachedDeviceManager.findDevice(device) + if (BluetoothUtils.getGroupId(cachedDevice) == groupId) { + cachedDevice + } else { + null + } + } + } } class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { @@ -269,6 +308,10 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID) override val secondaryGroupId: StateFlow<Int> = MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID) + override val primaryDevice: StateFlow<CachedBluetoothDevice?> + get() = MutableStateFlow(null) + override val secondaryDevice: StateFlow<CachedBluetoothDevice?> + get() = MutableStateFlow(null) override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap()) override suspend fun audioSharingAvailable(): Boolean = false 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 8c5a0851cc92..0f25ae208a69 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 @@ -209,6 +209,21 @@ class AudioSharingRepositoryTest { } @Test + fun primaryDeviceChange_emitValues() { + testScope.runTest { + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) + + val devices = mutableListOf<CachedBluetoothDevice?>() + underTest.primaryDevice.onEach { devices.add(it) }.launchIn(backgroundScope) + runCurrent() + triggerContentObserverChange() + runCurrent() + + Truth.assertThat(devices).containsExactly(null, cachedDevice2) + } + } + + @Test fun secondaryGroupIdChange_profileNotReady_assistantCallbackNotRegistered() { testScope.runTest { val groupIds = mutableListOf<Int?>() @@ -269,6 +284,29 @@ class AudioSharingRepositoryTest { } @Test + fun secondaryDeviceChange_emitValues() { + testScope.runTest { + `when`(broadcast.isProfileReady).thenReturn(true) + `when`(assistant.isProfileReady).thenReturn(true) + `when`(volumeControl.isProfileReady).thenReturn(true) + val devices = mutableListOf<CachedBluetoothDevice?>() + underTest.secondaryDevice.onEach { devices.add(it) }.launchIn(backgroundScope) + runCurrent() + triggerSourceAdded() + runCurrent() + triggerContentObserverChange() + runCurrent() + + Truth.assertThat(devices) + .containsExactly( + null, + cachedDevice2, + cachedDevice1, + ) + } + } + + @Test fun volumeMapChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) @@ -363,7 +401,7 @@ class AudioSharingRepositoryTest { TEST_GROUP_ID1 ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) - assistantCallbackCaptor.value.sourceAdded(device1, receiveState) + assistantCallbackCaptor.value.sourceAdded(device1) } private fun triggerSourceRemoved() { @@ -432,11 +470,9 @@ class AudioSharingRepositoryTest { onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID) } val sourceAdded: - BluetoothLeBroadcastAssistant.Callback.( - sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState - ) -> Unit = - { sink, state -> - onReceiveStateChanged(sink, TEST_SOURCE_ID, state) + BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit = + { sink -> + onSourceAdded(sink, TEST_SOURCE_ID, TEST_REASON) } val sourceRemoved: BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit = { sink -> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt index c9d147b6c81c..09d6ac6589ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt @@ -16,11 +16,16 @@ package com.android.systemui.volume.domain.interactor +import android.bluetooth.BluetoothDevice import android.media.AudioManager.STREAM_MUSIC import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase +import com.android.systemui.bluetooth.cachedBluetoothDeviceManager +import com.android.systemui.bluetooth.localBluetoothProfileManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos @@ -32,6 +37,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @@ -39,10 +46,23 @@ import org.junit.runner.RunWith class AudioSharingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() lateinit var underTest: AudioSharingInteractor + private val bluetoothDevice: BluetoothDevice = mock {} + private val cachedDevice: CachedBluetoothDevice = mock { + on { groupId }.thenReturn(TEST_GROUP_ID) + on { device }.thenReturn(bluetoothDevice) + } @Before fun setUp() { with(kosmos) { + whenever(cachedBluetoothDeviceManager.findDevice(bluetoothDevice)) + .thenReturn(cachedDevice) + val broadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant = mock { + on { allConnectedDevices }.thenReturn(listOf(bluetoothDevice)) + } + whenever(localBluetoothProfileManager.leAudioBroadcastAssistantProfile) + .thenReturn(broadcastAssistantProfile) + with(audioSharingRepository) { setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME)) } underTest = audioSharingInteractor } @@ -90,6 +110,35 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test + fun getPrimaryDevice() { + with(kosmos) { + testScope.runTest { + with(audioSharingRepository) { setPrimaryDevice(cachedDevice) } + underTest.handlePrimaryGroupChange() + + val primaryDevice by collectLastValue(underTest.primaryDevice) + runCurrent() + + Truth.assertThat(primaryDevice).isEqualTo(cachedDevice) + } + } + } + + @Test + fun getSecondaryDevice() { + with(kosmos) { + testScope.runTest { + with(audioSharingRepository) { setSecondaryDevice(cachedDevice) } + + val secondaryDevice by collectLastValue(underTest.secondaryDevice) + runCurrent() + + Truth.assertThat(secondaryDevice).isEqualTo(cachedDevice) + } + } + } + + @Test fun handlePrimaryGroupChange_setStreamVolume() { with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt new file mode 100644 index 000000000000..b34d7b8ec17d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt @@ -0,0 +1,91 @@ +/* + * 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.systemui.volume.panel.component.volume.slider.ui.viewmodel + +import android.bluetooth.BluetoothDevice +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.audioSharingRepository +import com.android.systemui.volume.domain.interactor.audioSharingInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class AudioSharingStreamSliderViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var stream: AudioSharingStreamSliderViewModel + + @Before + fun setUp() { + stream = audioSharingStreamSliderViewModel() + } + + private fun audioSharingStreamSliderViewModel(): AudioSharingStreamSliderViewModel { + return AudioSharingStreamSliderViewModel( + testScope.backgroundScope, + context, + kosmos.audioSharingInteractor, + kosmos.uiEventLogger, + kosmos.sliderHapticsViewModelFactory, + ) + } + + @Test + fun slider_media_inAudioSharing() = + with(kosmos) { + testScope.runTest { + val audioSharingSlider by collectLastValue(stream.slider) + + val bluetoothDevice: BluetoothDevice = mock {} + val cachedDevice: CachedBluetoothDevice = mock { + on { groupId }.thenReturn(123) + on { device }.thenReturn(bluetoothDevice) + on { name }.thenReturn("my headset 2") + } + audioSharingRepository.setSecondaryDevice(cachedDevice) + + audioSharingRepository.setInAudioSharing(true) + audioSharingRepository.setSecondaryGroupId(123) + + runCurrent() + + assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2") + assertThat(audioSharingSlider!!.icon) + .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt index 51cac6976362..9e8cde3bc936 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -23,19 +23,27 @@ import android.platform.test.annotations.EnableFlags import android.service.notification.ZenPolicy import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.audioSharingRepository import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @@ -146,4 +154,25 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { assertThat(notificationSlider!!.disabledMessage) .isEqualTo("Unavailable because ring is muted") } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_SHOW_AUDIO_SHARING_SLIDER_IN_VOLUME_PANEL) + fun slider_media_inAudioSharing() = + kosmos.runTest { + val mediaSlider by + collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider) + + val cachedDevice: CachedBluetoothDevice = mock { + on { groupId }.thenReturn(123) + on { name }.thenReturn("my headset 1") + } + + audioSharingRepository.setInAudioSharing(true) + audioSharingRepository.setPrimaryDevice(cachedDevice) + runCurrent() + + assertThat(mediaSlider!!.label).isEqualTo("my headset 1") + assertThat(mediaSlider!!.icon) + .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt index 7da041e7ef19..411288ff1274 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt @@ -22,6 +22,7 @@ import android.media.AudioManager.STREAM_MUSIC import androidx.annotation.IntRange import com.android.app.tracing.coroutines.launchTraced as launch import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.flags.Flags import com.android.settingslib.volume.data.repository.AudioSharingRepository import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX @@ -42,12 +43,19 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext interface AudioSharingInteractor { /** Audio sharing state on the device. */ val isInAudioSharing: Flow<Boolean> + /** Primary audio sharing device. */ + val primaryDevice: Flow<CachedBluetoothDevice?> + + /** Secondary audio sharing device. */ + val secondaryDevice: Flow<CachedBluetoothDevice?> + /** Audio sharing secondary headset volume changes. */ val volume: Flow<Int?> @@ -86,6 +94,11 @@ constructor( private val audioSharingRepository: AudioSharingRepository, ) : AudioSharingInteractor { override val isInAudioSharing: Flow<Boolean> = audioSharingRepository.inAudioSharing + override val primaryDevice: Flow<CachedBluetoothDevice?> + get() = audioSharingRepository.primaryDevice + + override val secondaryDevice: Flow<CachedBluetoothDevice?> + get() = audioSharingRepository.secondaryDevice override val volume: Flow<Int?> = combine(audioSharingRepository.secondaryGroupId, audioSharingRepository.volumeMap) { @@ -148,6 +161,8 @@ constructor( @SysUISingleton class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor { override val isInAudioSharing: Flow<Boolean> = flowOf(false) + override val primaryDevice: Flow<CachedBluetoothDevice?> = flowOf(null) + override val secondaryDevice: Flow<CachedBluetoothDevice?> = flowOf(null) override val volume: Flow<Int?> = emptyFlow() override val volumeMin: Int = EMPTY_VOLUME override val volumeMax: Int = EMPTY_VOLUME diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt index 1e4afc0ac5fe..a326da45a7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt @@ -21,6 +21,7 @@ import com.android.settingslib.volume.data.repository.AudioSystemRepository import com.android.settingslib.volume.domain.interactor.AudioModeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.Flags +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession import com.android.systemui.volume.panel.component.mediaoutput.shared.model.isTheSameSession @@ -44,6 +45,7 @@ constructor( mediaOutputInteractor: MediaOutputInteractor, audioModeInteractor: AudioModeInteractor, private val audioSystemRepository: AudioSystemRepository, + audioSharingInteractor: AudioSharingInteractor, ) { val volumePanelSliders: StateFlow<List<SliderType>> = @@ -51,7 +53,8 @@ constructor( mediaOutputInteractor.activeMediaDeviceSessions, mediaOutputInteractor.defaultActiveMediaSession.filterData(), audioModeInteractor.isOngoingCall, - ) { activeSessions, defaultSession, isOngoingCall -> + audioSharingInteractor.volume, + ) { activeSessions, defaultSession, isOngoingCall, audioSharingVolume -> coroutineScope { val viewModels = buildList { if (isOngoingCall) { @@ -61,8 +64,14 @@ constructor( if (defaultSession?.isTheSameSession(activeSessions.remote) == true) { addSession(activeSessions.remote) addStream(AudioManager.STREAM_MUSIC) + if (Flags.showAudioSharingSliderInVolumePanel()) { + audioSharingVolume?.let { addAudioSharingStream() } + } } else { addStream(AudioManager.STREAM_MUSIC) + if (Flags.showAudioSharingSliderInVolumePanel()) { + audioSharingVolume?.let { addAudioSharingStream() } + } addSession(activeSessions.remote) } @@ -89,13 +98,18 @@ constructor( // Hide other streams except STREAM_MUSIC if the isSingleVolume mode is on. This makes sure // the volume slider in volume panel is consistent with the volume slider inside system // settings app. - if (Flags.onlyShowMediaStreamSliderInSingleVolumeMode() && - audioSystemRepository.isSingleVolume && - stream != AudioManager.STREAM_MUSIC + if ( + Flags.onlyShowMediaStreamSliderInSingleVolumeMode() && + audioSystemRepository.isSingleVolume && + stream != AudioManager.STREAM_MUSIC ) { return } add(SliderType.Stream(AudioStream(stream))) } + + private fun MutableList<SliderType>.addAudioSharingStream() { + add(SliderType.AudioSharingStream) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt index 6129ce543e2e..f180744eac22 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt @@ -27,4 +27,7 @@ sealed interface SliderType { /** The represents media device casting volume. */ data class MediaDeviceCast(val session: MediaDeviceSession) : SliderType + + /** Represents the audio sharing volume stream. */ + data object AudioSharingStream : SliderType } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt new file mode 100644 index 000000000000..4ce9fe561aed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt @@ -0,0 +1,130 @@ +/* + * 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.systemui.volume.panel.component.volume.slider.ui.viewmodel + +import android.content.Context +import com.android.internal.logging.UiEventLogger +import com.android.systemui.Flags +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.res.R +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingStreamSliderViewModel +@AssistedInject +constructor( + @Assisted private val coroutineScope: CoroutineScope, + private val context: Context, + private val audioSharingInteractor: AudioSharingInteractor, + private val uiEventLogger: UiEventLogger, + private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) : SliderViewModel { + private val volumeChanges = MutableStateFlow<Int?>(null) + + override val slider: StateFlow<SliderState> = + combine(audioSharingInteractor.volume, audioSharingInteractor.secondaryDevice) { + volume, + device -> + val deviceName = device?.name ?: return@combine SliderState.Empty + if (volume == null) { + SliderState.Empty + } else { + State( + value = volume.toFloat(), + valueRange = + audioSharingInteractor.volumeMin.toFloat()..audioSharingInteractor + .volumeMax + .toFloat(), + icon = Icon.Resource(R.drawable.ic_volume_media_bt, null), + label = deviceName, + ) + } + } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) + + init { + volumeChanges + .filterNotNull() + .onEach { audioSharingInteractor.setStreamVolume(it) } + .launchIn(coroutineScope) + } + + override fun onValueChanged(state: SliderState, newValue: Float) { + val audioViewModel = state as? State + audioViewModel ?: return + volumeChanges.tryEmit(newValue.roundToInt()) + } + + override fun onValueChangeFinished() { + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_AUDIO_SHARING_SLIDER_TOUCHED) + } + + override fun toggleMuted(state: SliderState) {} + + override fun getSliderHapticsViewModelFactory(): SliderHapticsViewModel.Factory? = + if (Flags.hapticsForComposeSliders() && slider.value != SliderState.Empty) { + hapticsViewModelFactory + } else { + null + } + + private data class State( + override val value: Float, + override val valueRange: ClosedFloatingPointRange<Float>, + override val icon: Icon, + override val label: String, + ) : SliderState { + override val isEnabled: Boolean + get() = true + + override val a11yStep: Int + get() = 1 + + override val disabledMessage: String? + get() = null + + override val isMutable: Boolean + get() = false + + override val a11yClickDescription: String? + get() = null + + override val a11yStateDescription: String? + get() = null + } + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 5b8d9b045475..3f5b899d27f9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -21,6 +21,7 @@ import android.media.AudioManager import android.util.Log import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel @@ -31,6 +32,8 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.kotlin.combine +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted @@ -42,7 +45,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -59,6 +61,7 @@ constructor( private val context: Context, private val audioVolumeInteractor: AudioVolumeInteractor, private val zenModeInteractor: ZenModeInteractor, + private val audioSharingInteractor: AudioSharingInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, @@ -68,14 +71,6 @@ constructor( private val streamsAffectedByRing = setOf(AudioManager.STREAM_RING, AudioManager.STREAM_NOTIFICATION) private val audioStream = audioStreamWrapper.audioStream - private val iconsByStream = - mapOf( - AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_music_note, - AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_call, - AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_ring_volume, - AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer, - AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm, - ) private val labelsByStream = mapOf( AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music, @@ -104,9 +99,18 @@ constructor( audioVolumeInteractor.canChangeVolume(audioStream), audioVolumeInteractor.ringerMode, streamDisabledMessage(), - ) { model, isEnabled, ringerMode, streamDisabledMessage -> + audioSharingInteractor.isInAudioSharing, + audioSharingInteractor.primaryDevice, + ) { model, isEnabled, ringerMode, streamDisabledMessage, isInAudioSharing, primaryDevice + -> volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) - model.toState(isEnabled, ringerMode, streamDisabledMessage) + model.toState( + isEnabled, + ringerMode, + streamDisabledMessage, + isInAudioSharing, + primaryDevice, + ) } .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) @@ -149,14 +153,15 @@ constructor( isEnabled: Boolean, ringerMode: RingerMode, disabledMessage: String?, + inAudioSharing: Boolean, + primaryDevice: CachedBluetoothDevice?, ): State { - val label = - labelsByStream[audioStream]?.let(context::getString) - ?: error("No label for the stream: $audioStream") + val label = getLabel(inAudioSharing, primaryDevice) + val icon = getIcon(ringerMode, inAudioSharing) return State( value = volume.toFloat(), valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), - icon = getIcon(ringerMode), + icon = icon, label = label, disabledMessage = disabledMessage, isEnabled = isEnabled, @@ -224,7 +229,22 @@ constructor( } } - private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon { + private fun AudioStreamModel.getLabel( + inAudioSharing: Boolean, + primaryDevice: CachedBluetoothDevice?, + ): String = + if ( + Flags.showAudioSharingSliderInVolumePanel() && + audioStream.value == AudioManager.STREAM_MUSIC && + inAudioSharing + ) { + primaryDevice?.name ?: context.getString(R.string.stream_music) + } else { + labelsByStream[audioStream]?.let(context::getString) + ?: error("No label for the stream: $audioStream") + } + + private fun AudioStreamModel.getIcon(ringerMode: RingerMode, inAudioSharing: Boolean): Icon { val iconRes = if (isAffectedByMute && isMuted) { if (audioStream.value in streamsAffectedByRing) { @@ -234,18 +254,36 @@ constructor( R.drawable.ic_volume_off } } else { - R.drawable.ic_volume_off + if ( + Flags.showAudioSharingSliderInVolumePanel() && + audioStream.value == AudioManager.STREAM_MUSIC && + inAudioSharing + ) { + R.drawable.ic_volume_media_bt_mute + } else R.drawable.ic_volume_off } } else { - iconsByStream[audioStream] - ?: run { - Log.wtf(TAG, "No icon for the stream: $audioStream") - R.drawable.ic_music_note - } + getIconByStream(audioStream, inAudioSharing) } return Icon.Resource(iconRes, null) } + private fun getIconByStream(audioStream: AudioStream, inAudioSharing: Boolean): Int = + when (audioStream.value) { + AudioManager.STREAM_MUSIC -> + if (Flags.showAudioSharingSliderInVolumePanel() && inAudioSharing) { + R.drawable.ic_volume_media_bt + } else R.drawable.ic_music_note + AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_call + AudioManager.STREAM_RING -> R.drawable.ic_ring_volume + AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer + AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm + else -> { + Log.wtf(TAG, "No icon for the stream: $audioStream") + R.drawable.ic_music_note + } + } + private val AudioStreamModel.volumeRange: IntRange get() = minVolume..maxVolume diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index 96afbc1feaaf..28f11050fa4a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.volume.ui.viewmodel +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.settingslib.volume.domain.interactor.AudioModeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor @@ -23,6 +24,7 @@ import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession import com.android.systemui.volume.panel.component.volume.domain.interactor.AudioSlidersInteractor import com.android.systemui.volume.panel.component.volume.domain.model.SliderType +import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioSharingStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel @@ -45,7 +47,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import com.android.app.tracing.coroutines.launchTraced as launch /** * Controls the behaviour of the whole audio @@ -61,6 +62,7 @@ constructor( mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory, private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory, + private val audioSharingStreamSliderViewModelFactory: AudioSharingStreamSliderViewModel.Factory, audioModeInteractor: AudioModeInteractor, streamsInteractor: AudioSlidersInteractor, ) { @@ -108,6 +110,7 @@ constructor( is SliderType.Stream -> createStreamViewModel(type.stream) is SliderType.MediaDeviceCast -> createSessionViewModel(type.session) + is SliderType.AudioSharingStream -> createAudioSharingViewModel() } } emit(viewModels) @@ -138,11 +141,15 @@ constructor( } private fun CoroutineScope.createStreamViewModel( - stream: AudioStream, + stream: AudioStream ): AudioStreamSliderViewModel { return streamSliderViewModelFactory.create( AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream), this, ) } + + private fun CoroutineScope.createAudioSharingViewModel(): AudioSharingStreamSliderViewModel { + return audioSharingStreamSliderViewModelFactory.create(this) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt index 8b8714fcca8c..d3a4fe86e827 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt @@ -34,6 +34,8 @@ enum class VolumePanelUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "The notification volume slider is touched") VOLUME_PANEL_NOTIFICATION_SLIDER_TOUCHED(1642), @UiEvent(doc = "The alarm volume slider is touched") VOLUME_PANEL_ALARM_SLIDER_TOUCHED(1643), + @UiEvent(doc = "The audio sharing volume slider is touched") + VOLUME_PANEL_AUDIO_SHARING_SLIDER_TOUCHED(2068), @UiEvent(doc = "Live caption toggle is shown") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_SHOWN(1644), @UiEvent(doc = "Live caption toggle is gone") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE(1645), @UiEvent(doc = "Live caption toggle is clicked") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_CLICKED(1646), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt index eef89e7dac68..3d58cf58d7b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt @@ -18,10 +18,12 @@ package com.android.systemui.bluetooth import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import org.mockito.kotlin.mock var Kosmos.localBluetoothManager: LocalBluetoothManager? by Kosmos.Fixture { - mock { whenever(cachedDeviceManager).thenReturn(cachedBluetoothDeviceManager) } + mock { + on { cachedDeviceManager }.thenReturn(cachedBluetoothDeviceManager) + on { profileManager }.thenReturn(localBluetoothProfileManager) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothProfileManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothProfileManagerKosmos.kt new file mode 100644 index 000000000000..34d7848a70aa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothProfileManagerKosmos.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.systemui.bluetooth + +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.localBluetoothProfileManager: LocalBluetoothProfileManager by Kosmos.Fixture { mock {} } 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 5da6ee95234c..6e76cf34c652 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,6 +16,7 @@ package com.android.systemui.volume.data.repository +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.volume.data.repository.AudioSharingRepository import com.android.settingslib.volume.data.repository.GroupIdToVolumes import kotlinx.coroutines.flow.MutableStateFlow @@ -28,11 +29,17 @@ class FakeAudioSharingRepository : AudioSharingRepository { MutableStateFlow(TEST_GROUP_ID_INVALID) private val mutableSecondaryGroupId: MutableStateFlow<Int> = MutableStateFlow(TEST_GROUP_ID_INVALID) + private val mutablePrimaryDevice: MutableStateFlow<CachedBluetoothDevice?> = + MutableStateFlow(null) + private val mutableSecondaryDevice: MutableStateFlow<CachedBluetoothDevice?> = + MutableStateFlow(null) private val mutableVolumeMap: MutableStateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap()) override val inAudioSharing: StateFlow<Boolean> = mutableInAudioSharing override val primaryGroupId: StateFlow<Int> = mutablePrimaryGroupId override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId + override val primaryDevice: StateFlow<CachedBluetoothDevice?> = mutablePrimaryDevice + override val secondaryDevice: StateFlow<CachedBluetoothDevice?> = mutableSecondaryDevice override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap override suspend fun audioSharingAvailable(): Boolean = mutableAvailable @@ -55,6 +62,14 @@ class FakeAudioSharingRepository : AudioSharingRepository { mutableSecondaryGroupId.value = groupId } + fun setPrimaryDevice(device: CachedBluetoothDevice?) { + mutablePrimaryDevice.value = device + } + + fun setSecondaryDevice(device: CachedBluetoothDevice?) { + mutableSecondaryDevice.value = device + } + fun setVolumeMap(volumeMap: GroupIdToVolumes) { mutableVolumeMap.value = volumeMap } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/FakeAudioSharingInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/FakeAudioSharingInteractor.kt index 1fb5e77a3210..78cafbf99f49 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/FakeAudioSharingInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/FakeAudioSharingInteractor.kt @@ -19,10 +19,11 @@ package com.android.systemui.volume.domain.interactor import android.content.Context import androidx.annotation.IntRange import com.android.dream.lowlight.dagger.qualifiers.Application +import com.android.settingslib.bluetooth.CachedBluetoothDevice import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeAudioSharingInteractor : AudioSharingInteractor { +class FakeAudioSharingInteractor() : AudioSharingInteractor { private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false) private val mutableVolume: MutableStateFlow<Int?> = MutableStateFlow(null) private var audioSharingVolumeBarAvailable = false @@ -31,6 +32,8 @@ class FakeAudioSharingInteractor : AudioSharingInteractor { override val volume: Flow<Int?> = mutableVolume override val volumeMin: Int = AUDIO_SHARING_VOLUME_MIN override val volumeMax: Int = AUDIO_SHARING_VOLUME_MAX + override val primaryDevice = MutableStateFlow<CachedBluetoothDevice?>(null) + override val secondaryDevice = MutableStateFlow<CachedBluetoothDevice?>(null) override suspend fun audioSharingVolumeBarAvailable(@Application context: Context): Boolean = audioSharingVolumeBarAvailable @@ -54,6 +57,14 @@ class FakeAudioSharingInteractor : AudioSharingInteractor { audioSharingVolumeBarAvailable = available } + fun setPrimaryDevice(device: CachedBluetoothDevice?) { + primaryDevice.value = device + } + + fun setSecondaryDevice(device: CachedBluetoothDevice?) { + secondaryDevice.value = device + } + companion object { const val AUDIO_SHARING_VOLUME_MIN = 0 const val AUDIO_SHARING_VOLUME_MAX = 255 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt index 3bc920edd948..88734cd3b000 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.volume.data.repository.audioSystemRepository import com.android.systemui.volume.domain.interactor.audioModeInteractor +import com.android.systemui.volume.domain.interactor.audioSharingInteractor import com.android.systemui.volume.mediaOutputInteractor val Kosmos.audioSlidersInteractor by @@ -29,5 +30,6 @@ val Kosmos.audioSlidersInteractor by mediaOutputInteractor, audioModeInteractor, audioSystemRepository, + audioSharingInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt new file mode 100644 index 000000000000..96bc9722635a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.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.systemui.volume.panel.component.volume.slider.ui.viewmodel + +import android.content.applicationContext +import com.android.internal.logging.uiEventLogger +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.domain.interactor.audioSharingInteractor +import kotlinx.coroutines.CoroutineScope + +val Kosmos.audioSharingStreamSliderViewModelFactory by + Kosmos.Fixture { + object : AudioSharingStreamSliderViewModel.Factory { + override fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel { + return AudioSharingStreamSliderViewModel( + coroutineScope, + applicationContext, + audioSharingInteractor, + uiEventLogger, + sliderHapticsViewModelFactory, + ) + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt index a78670d7f1cc..88c716e0ab10 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.internal.logging.uiEventLogger import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.volume.domain.interactor.audioSharingInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor import com.android.systemui.volume.shared.volumePanelLogger import kotlinx.coroutines.CoroutineScope @@ -39,6 +40,7 @@ val Kosmos.audioStreamSliderViewModelFactory by applicationContext, audioVolumeInteractor, zenModeInteractor, + audioSharingInteractor, uiEventLogger, volumePanelLogger, sliderHapticsViewModelFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/VolumeSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/VolumeSlidersViewModelKosmos.kt index 6e848ce26d9b..a6a4f8368941 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/VolumeSlidersViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/VolumeSlidersViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.volume.domain.interactor.audioModeInteractor import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.component.volume.domain.interactor.audioSlidersInteractor +import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.audioSharingStreamSliderViewModelFactory import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.audioStreamSliderViewModelFactory import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.castVolumeSliderViewModelFactory @@ -33,6 +34,7 @@ val Kosmos.audioVolumeComponentViewModel by mediaDeviceSessionInteractor, audioStreamSliderViewModelFactory, castVolumeSliderViewModelFactory, + audioSharingStreamSliderViewModelFactory, audioModeInteractor, audioSlidersInteractor, ) |