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

Commit e40423d5 authored by Chelsea Hao's avatar Chelsea Hao Committed by Android (Google) Code Review
Browse files

Merge changes I7495bab9,Iebc43ad2 into main

* changes:
  Start / stop broadcast by clicking on plus or checkmark icon.
  Replace gear button with "+" button for audio sharing available devices, and check mark for devices in audio sharing session.
parents feda6dbd 11721ddc
Loading
Loading
Loading
Loading
+121 −5
Original line number Diff line number Diff line
@@ -18,12 +18,20 @@ package com.android.systemui.bluetooth.qsdialog

import android.bluetooth.BluetoothLeBroadcast
import android.bluetooth.BluetoothLeBroadcastMetadata
import android.content.ContentResolver
import android.content.applicationContext
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.settingslib.bluetooth.VolumeControlProfile
import com.android.settingslib.volume.shared.AudioSharingLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -38,10 +46,16 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -50,8 +64,11 @@ import org.mockito.kotlin.any
class AudioSharingInteractorTest : SysuiTestCase() {
    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
    private val kosmos = testKosmos()

    @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast

    @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata

    @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
    private lateinit var underTest: AudioSharingInteractor

@@ -157,13 +174,15 @@ class AudioSharingInteractorTest : SysuiTestCase() {
    fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                runCurrent()

                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                job.cancel()
@@ -174,15 +193,14 @@ class AudioSharingInteractorTest : SysuiTestCase() {
    fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                verify(localBluetoothLeBroadcast)
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                runCurrent()

                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
@@ -194,13 +212,15 @@ class AudioSharingInteractorTest : SysuiTestCase() {
    fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
        with(kosmos) {
            testScope.runTest {
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                    localBluetoothLeBroadcast
                )
                val job = launch { underTest.handleAudioSourceWhenReady() }
                runCurrent()
                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                runCurrent()
                verify(localBluetoothLeBroadcast)
                    .registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
@@ -211,4 +231,100 @@ class AudioSharingInteractorTest : SysuiTestCase() {
                job.cancel()
            }
        }

    @Test
    fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() =
        with(kosmos) {
            testScope.runTest {
                val (broadcast, repository) = setupRepositoryImpl()
                val interactor =
                    object :
                        AudioSharingInteractorImpl(
                            applicationContext,
                            localBluetoothManager,
                            repository,
                            testDispatcher,
                        ) {
                        override suspend fun audioSharingAvailable() = true
                    }
                val job = launch { interactor.handleAudioSourceWhenReady() }
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
                // Verify source is not added
                verify(repository, never()).addSource()
                job.cancel()
            }
        }

    @Test
    fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() =
        with(kosmos) {
            testScope.runTest {
                val (broadcast, repository) = setupRepositoryImpl()
                val interactor =
                    object :
                        AudioSharingInteractorImpl(
                            applicationContext,
                            localBluetoothManager,
                            repository,
                            testDispatcher,
                        ) {
                        override suspend fun audioSharingAvailable() = true
                    }
                val job = launch { interactor.handleAudioSourceWhenReady() }
                runCurrent()
                // Verify callback registered for onBroadcastStartedOrStopped
                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
                // Audio sharing started, trigger onBroadcastStarted
                whenever(broadcast.isEnabled(null)).thenReturn(true)
                callbackCaptor.value.onBroadcastStarted(0, 0)
                runCurrent()
                // Verify callback registered for onBroadcastMetadataChanged
                verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture())
                runCurrent()
                // Trigger onBroadcastMetadataChanged (ready to add source)
                callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
                runCurrent()
                // Verify source added
                verify(repository).addSource()
                job.cancel()
            }
        }

    private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> {
        with(kosmos) {
            val broadcast =
                mock<LocalBluetoothLeBroadcast> {
                    on { isProfileReady } doReturn true
                    on { isEnabled(null) } doReturn false
                }
            val assistant =
                mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true }
            val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true }
            val profileManager =
                mock<LocalBluetoothProfileManager> {
                    on { leAudioBroadcastProfile } doReturn broadcast
                    on { leAudioBroadcastAssistantProfile } doReturn assistant
                    on { volumeControlProfile } doReturn volumeControl
                }
            whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
            whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {})

            val repository =
                AudioSharingRepositoryImpl(
                    localBluetoothManager,
                    com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl(
                        mock<ContentResolver> {},
                        localBluetoothManager,
                        testScope.backgroundScope,
                        testScope.testScheduler,
                        mock<AudioSharingLogger> {},
                    ),
                    testDispatcher,
                )
            return Pair(broadcast, spy(repository))
        }
    }
}
+22 −0
Original line number Diff line number Diff line
@@ -110,6 +110,28 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
            }
        }

    @Test
    fun testStopAudioSharing() =
        with(kosmos) {
            testScope.runTest {
                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
                audioSharingRepository.setAudioSharingAvailable(true)
                underTest.stopAudioSharing()
                verify(leAudioBroadcastProfile).stopLatestBroadcast()
            }
        }

    @Test
    fun testStopAudioSharing_flagOff_doNothing() =
        with(kosmos) {
            testScope.runTest {
                audioSharingRepository.setAudioSharingAvailable(false)
                underTest.stopAudioSharing()
                verify(leAudioBroadcastProfile, never()).stopLatestBroadcast()
            }
        }

    @Test
    fun testAddSource_flagOff_doesNothing() =
        with(kosmos) {
+39 −5
Original line number Diff line number Diff line
@@ -15,14 +15,15 @@
 */
package com.android.systemui.bluetooth.qsdialog

import androidx.test.ext.junit.runners.AndroidJUnit4
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -48,6 +49,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
    private lateinit var notConnectedDeviceItem: DeviceItem
    private lateinit var connectedMediaDeviceItem: DeviceItem
    private lateinit var connectedOtherDeviceItem: DeviceItem
    private lateinit var audioSharingDeviceItem: DeviceItem
    @Mock private lateinit var dialog: SystemUIDialog

    @Before
@@ -59,7 +61,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
                deviceName = DEVICE_NAME,
                connectionSummary = DEVICE_CONNECTION_SUMMARY,
                iconWithDescription = null,
                background = null
                background = null,
            )
        notConnectedDeviceItem =
            DeviceItem(
@@ -68,7 +70,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
                deviceName = DEVICE_NAME,
                connectionSummary = DEVICE_CONNECTION_SUMMARY,
                iconWithDescription = null,
                background = null
                background = null,
            )
        connectedMediaDeviceItem =
            DeviceItem(
@@ -77,7 +79,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
                deviceName = DEVICE_NAME,
                connectionSummary = DEVICE_CONNECTION_SUMMARY,
                iconWithDescription = null,
                background = null
                background = null,
            )
        connectedOtherDeviceItem =
            DeviceItem(
@@ -86,7 +88,16 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
                deviceName = DEVICE_NAME,
                connectionSummary = DEVICE_CONNECTION_SUMMARY,
                iconWithDescription = null,
                background = null
                background = null,
            )
        audioSharingDeviceItem =
            DeviceItem(
                type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
                deviceName = DEVICE_NAME,
                connectionSummary = DEVICE_CONNECTION_SUMMARY,
                iconWithDescription = null,
                background = null,
            )
        actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
    }
@@ -135,6 +146,29 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
        }
    }

    @Test
    fun onActionIconClick_onIntent() {
        with(kosmos) {
            testScope.runTest {
                var onIntentCalledOnAddress = ""
                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
                actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) {
                    onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address
                }
                assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS)
            }
        }
    }

    @Test(expected = IllegalArgumentException::class)
    fun onActionIconClick_audioSharingDeviceType_throwException() {
        with(kosmos) {
            testScope.runTest {
                actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {}
            }
        }
    }

    private companion object {
        const val DEVICE_NAME = "device"
        const val DEVICE_CONNECTION_SUMMARY = "active"
+25 −0
Original line number Diff line number Diff line
@@ -104,6 +104,31 @@ constructor(
        }
    }

    override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
        withContext(backgroundDispatcher) {
            if (!audioSharingInteractor.audioSharingAvailable()) {
                return@withContext deviceItemActionInteractorImpl.onActionIconClick(
                    deviceItem,
                    onIntent,
                )
            }

            when (deviceItem.type) {
                DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
                    uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED)
                    audioSharingInteractor.stopAudioSharing()
                }
                DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
                    uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED)
                    audioSharingInteractor.startAudioSharing()
                }
                else -> {
                    deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent)
                }
            }
        }
    }

    private fun inSharingAndDeviceNoSource(
        inAudioSharing: Boolean,
        deviceItem: DeviceItem,
+16 −1
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
@@ -54,6 +55,8 @@ interface AudioSharingInteractor {

    suspend fun startAudioSharing()

    suspend fun stopAudioSharing()

    suspend fun audioSharingAvailable(): Boolean

    suspend fun qsDialogImprovementAvailable(): Boolean
@@ -61,7 +64,7 @@ interface AudioSharingInteractor {

@SysUISingleton
@OptIn(ExperimentalCoroutinesApi::class)
class AudioSharingInteractorImpl
open class AudioSharingInteractorImpl
@Inject
constructor(
    private val context: Context,
@@ -99,6 +102,9 @@ constructor(
            if (audioSharingAvailable()) {
                audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                    isAudioSharingOn
                        // Skip the default value, we only care about adding source for newly
                        // started audio sharing session
                        .drop(1)
                        .mapNotNull { audioSharingOn ->
                            if (audioSharingOn) {
                                // onBroadcastMetadataChanged could emit multiple times during one
@@ -145,6 +151,13 @@ constructor(
        audioSharingRepository.startAudioSharing()
    }

    override suspend fun stopAudioSharing() {
        if (!audioSharingAvailable()) {
            return
        }
        audioSharingRepository.stopAudioSharing()
    }

    // TODO(b/367965193): Move this after flags rollout
    override suspend fun audioSharingAvailable(): Boolean {
        return audioSharingRepository.audioSharingAvailable()
@@ -181,6 +194,8 @@ class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingIntera

    override suspend fun startAudioSharing() {}

    override suspend fun stopAudioSharing() {}

    override suspend fun audioSharingAvailable(): Boolean = false

    override suspend fun qsDialogImprovementAvailable(): Boolean = false
Loading