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

Commit 554cd4a5 authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

Merge "Show two volume slider when audio sharing" into main

parents 68673d19 1be83fe2
Loading
Loading
Loading
Loading
+54 −11
Original line number Diff line number Diff line
@@ -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(
    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
+42 −6
Original line number Diff line number Diff line
@@ -208,6 +208,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 {
@@ -268,6 +283,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 {
@@ -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 ->
+49 −0
Original line number Diff line number Diff line
@@ -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
        }
@@ -89,6 +109,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) {
+91 −0
Original line number Diff line number Diff line
/*
 * 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))
            }
        }
}
+29 −0
Original line number Diff line number Diff line
@@ -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))
        }
}
Loading