summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt251
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt131
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractor.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt55
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BluetoothAdapterKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/CachedBluetoothDeviceManagerKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt27
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt21
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioModeInteractorKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeKosmos.kt)5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt40
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractorKosmos.kt23
15 files changed, 652 insertions, 15 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
new file mode 100644
index 000000000000..632196ccf66d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.domain.interactor
+
+import android.bluetooth.BluetoothDevice
+import android.graphics.drawable.TestStubDrawable
+import android.media.AudioDeviceInfo
+import android.media.AudioDevicePort
+import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.R
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.media.BluetoothMediaDevice
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.media.PhoneMediaDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.bluetoothAdapter
+import com.android.systemui.bluetooth.cachedBluetoothDeviceManager
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.domain.model.AudioOutputDevice
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.localMediaRepository
+import com.android.systemui.volume.mediaControllerRepository
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AudioOutputInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ lateinit var underTest: AudioOutputInteractor
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ underTest = audioOutputInteractor
+
+ with(context.orCreateTestableResources) {
+ addOverride(R.drawable.ic_headphone, testIcon)
+ addOverride(R.drawable.ic_smartphone, testIcon)
+ addOverride(R.drawable.ic_media_speaker_device, testIcon)
+
+ addOverride(com.android.internal.R.drawable.ic_bt_hearing_aid, testIcon)
+ }
+ }
+ }
+
+ @Test
+ fun inCall_builtIn_returnsCommunicationDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ with(audioRepository) {
+ setMode(AudioManager.MODE_IN_CALL)
+ setCommunicationDevice(builtInDevice)
+ }
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.BuiltIn::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("built_in")
+ }
+ }
+ }
+
+ @Test
+ fun inCall_wired_returnsCommunicationDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ with(audioRepository) {
+ setMode(AudioManager.MODE_IN_CALL)
+ setCommunicationDevice(wiredDevice)
+ }
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.Wired::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("wired")
+ }
+ }
+ }
+
+ @Test
+ fun inCall_bluetooth_returnsCommunicationDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ with(audioRepository) {
+ setMode(AudioManager.MODE_IN_CALL)
+ setCommunicationDevice(btDevice)
+ }
+ val bluetoothDevice: BluetoothDevice = mock {
+ whenever(address).thenReturn(btDevice.address)
+ }
+ val cachedBluetoothDevice: CachedBluetoothDevice = mock {
+ whenever(address).thenReturn(btDevice.address)
+ whenever(name).thenReturn(btDevice.productName.toString())
+ whenever(isHearingAidDevice).thenReturn(true)
+ }
+ whenever(bluetoothAdapter.getRemoteDevice(eq(btDevice.address)))
+ .thenReturn(bluetoothDevice)
+ whenever(cachedBluetoothDeviceManager.findDevice(any()))
+ .thenReturn(cachedBluetoothDevice)
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.Bluetooth::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("bt")
+ }
+ }
+ }
+
+ @Test
+ fun notInCall_builtIn_returnsMediaDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ audioRepository.setMode(AudioManager.MODE_NORMAL)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
+ localMediaRepository.updateCurrentConnectedDevice(builtInMediaDevice)
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.BuiltIn::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("built_in_media")
+ }
+ }
+ }
+
+ @Test
+ fun notInCall_wired_returnsMediaDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ audioRepository.setMode(AudioManager.MODE_NORMAL)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
+ localMediaRepository.updateCurrentConnectedDevice(wiredMediaDevice)
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.Wired::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("wired_media")
+ }
+ }
+ }
+
+ @Test
+ fun notInCall_bluetooth_returnsMediaDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ audioRepository.setMode(AudioManager.MODE_NORMAL)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
+ localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice)
+
+ val device by collectLastValue(underTest.currentAudioDevice)
+
+ runCurrent()
+
+ assertThat(device).isInstanceOf(AudioOutputDevice.Bluetooth::class.java)
+ assertThat(device!!.icon).isEqualTo(testIcon)
+ assertThat(device!!.name).isEqualTo("bt_media")
+ }
+ }
+ }
+
+ private companion object {
+ val testIcon = TestStubDrawable()
+ val builtInDevice =
+ AudioDeviceInfo(
+ AudioDevicePort.createForTesting(
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ "built_in",
+ ""
+ )
+ )
+ val wiredDevice =
+ AudioDeviceInfo(
+ AudioDevicePort.createForTesting(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, "wired", "")
+ )
+ val btDevice =
+ AudioDeviceInfo(
+ AudioDevicePort.createForTesting(
+ AudioDeviceInfo.TYPE_BLE_HEADSET,
+ "bt",
+ "test_address"
+ )
+ )
+ val builtInMediaDevice: MediaDevice =
+ mock<PhoneMediaDevice> {
+ whenever(name).thenReturn("built_in_media")
+ whenever(icon).thenReturn(testIcon)
+ }
+ val wiredMediaDevice: MediaDevice =
+ mock<PhoneMediaDevice> {
+ whenever(deviceType)
+ .thenReturn(MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE)
+ whenever(name).thenReturn("wired_media")
+ whenever(icon).thenReturn(testIcon)
+ }
+ val bluetoothMediaDevice: MediaDevice =
+ mock<BluetoothMediaDevice> {
+ whenever(name).thenReturn("bt_media")
+ whenever(icon).thenReturn(testIcon)
+ val cachedBluetoothDevice: CachedBluetoothDevice = mock {
+ whenever(isHearingAidDevice).thenReturn(true)
+ }
+ whenever(cachedDevice).thenReturn(cachedBluetoothDevice)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index fa79e7fc9026..675136c7cf26 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyInteractor
import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyRepository
import com.android.systemui.testKosmos
-import com.android.systemui.volume.audioRepository
+import com.android.systemui.volume.data.repository.audioRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt
index 8bb36724d1d8..89acbc8f1abd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt
@@ -25,8 +25,8 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
-import com.android.systemui.volume.audioModeInteractor
-import com.android.systemui.volume.audioRepository
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.domain.interactor.audioModeInteractor
import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -56,7 +56,7 @@ class AudioModeLoggerStartableTest : SysuiTestCase() {
AudioModeLoggerStartable(
applicationCoroutineScope,
uiEventLogger,
- audioModeInteractor
+ audioModeInteractor,
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt
index da0a2295143b..96b4dae7bb87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt
@@ -24,8 +24,8 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
-import com.android.systemui.volume.audioModeInteractor
-import com.android.systemui.volume.audioRepository
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.domain.interactor.audioModeInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
new file mode 100644
index 000000000000..ed446996a5f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.domain.interactor
+
+import android.bluetooth.BluetoothAdapter
+import android.media.AudioDeviceInfo
+import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES
+import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.media.BluetoothMediaDevice
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.media.MediaDevice.MediaDeviceType
+import com.android.settingslib.volume.data.repository.AudioRepository
+import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.domain.model.AudioOutputDevice
+import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Provides a currently active audio device data. */
+@VolumePanelScope
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioOutputInteractor
+@Inject
+constructor(
+ audioRepository: AudioRepository,
+ audioModeInteractor: AudioModeInteractor,
+ @VolumePanelScope scope: CoroutineScope,
+ @Background backgroundCoroutineContext: CoroutineContext,
+ private val localBluetoothManager: LocalBluetoothManager?,
+ private val bluetoothAdapter: BluetoothAdapter?,
+ private val deviceIconInteractor: DeviceIconInteractor,
+ private val mediaOutputInteractor: MediaOutputInteractor,
+ private val localMediaRepositoryFactory: LocalMediaRepositoryFactory,
+) {
+
+ val currentAudioDevice: Flow<AudioOutputDevice> =
+ audioModeInteractor.isOngoingCall
+ .flatMapLatest { isOngoingCall ->
+ if (isOngoingCall) {
+ audioRepository.communicationDevice.map { communicationDevice ->
+ communicationDevice?.toAudioOutputDevice()
+ }
+ } else {
+ mediaOutputInteractor.defaultActiveMediaSession
+ .flatMapLatest {
+ localMediaRepositoryFactory
+ .create(it?.packageName)
+ .currentConnectedDevice
+ }
+ .map { mediaDevice -> mediaDevice?.toAudioOutputDevice() }
+ }
+ }
+ .map { it ?: AudioOutputDevice.Unknown }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(scope, SharingStarted.Eagerly, AudioOutputDevice.Unknown)
+
+ private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice {
+ if (type == TYPE_WIRED_HEADPHONES || type == TYPE_WIRED_HEADSET) {
+ return AudioOutputDevice.Wired(
+ name = productName.toString(),
+ icon = deviceIconInteractor.loadIcon(type),
+ )
+ }
+ val cachedBluetoothDevice: CachedBluetoothDevice? =
+ if (address.isEmpty() || localBluetoothManager == null || bluetoothAdapter == null) {
+ null
+ } else {
+ val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
+ localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)
+ }
+ return cachedBluetoothDevice?.let {
+ AudioOutputDevice.Bluetooth(
+ name = it.name,
+ icon = deviceIconInteractor.loadIcon(it),
+ cachedBluetoothDevice = it,
+ )
+ }
+ ?: AudioOutputDevice.BuiltIn(
+ name = productName.toString(),
+ icon = deviceIconInteractor.loadIcon(type),
+ )
+ }
+
+ private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice {
+ return when {
+ this is BluetoothMediaDevice ->
+ AudioOutputDevice.Bluetooth(
+ name = name,
+ icon = icon,
+ cachedBluetoothDevice = cachedDevice,
+ )
+ deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE ->
+ AudioOutputDevice.Wired(
+ name = name,
+ icon = icon,
+ )
+ else ->
+ AudioOutputDevice.BuiltIn(
+ name = name,
+ icon = icon,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractor.kt
new file mode 100644
index 000000000000..a2f7d4a22e08
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractor.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.domain.interactor
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.media.AudioDeviceInfo
+import com.android.settingslib.R
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.media.DeviceIconUtil
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+
+/** Utility class to load an icon for a [CachedBluetoothDevice]. */
+@VolumePanelScope
+@SuppressLint("UseCompatLoadingForDrawables")
+class DeviceIconInteractor @Inject constructor(@Application private val context: Context) {
+
+ private val iconUtil: DeviceIconUtil = DeviceIconUtil(context)
+
+ fun loadIcon(@AudioDeviceInfo.AudioDeviceType type: Int): Drawable? =
+ context.getDrawable(iconUtil.getIconResIdFromAudioDeviceType(type))
+
+ fun loadIcon(cachedDevice: CachedBluetoothDevice): Drawable? {
+ return if (BluetoothUtils.isAdvancedUntetheredDevice(cachedDevice.device))
+ context.getDrawable(R.drawable.ic_earbuds_advanced)
+ else BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).first
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt
new file mode 100644
index 000000000000..ba0b08210799
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.domain.model
+
+import android.graphics.drawable.Drawable
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+
+/** Models an audio output device. */
+sealed interface AudioOutputDevice {
+
+ val name: String
+ val icon: Drawable?
+
+ /** Models a built audio output device. */
+ data class BuiltIn(
+ override val name: String,
+ override val icon: Drawable?,
+ ) : AudioOutputDevice
+
+ /** Models a wired audio output device. */
+ data class Wired(
+ override val name: String,
+ override val icon: Drawable?,
+ ) : AudioOutputDevice
+
+ /** Models a bluetooth audio output device. */
+ data class Bluetooth(
+ override val name: String,
+ override val icon: Drawable?,
+ val cachedBluetoothDevice: CachedBluetoothDevice,
+ ) : AudioOutputDevice
+
+ /** Models a state when the current audio output device is unknown. */
+ data object Unknown : AudioOutputDevice {
+ override val name: String
+ get() = error("Unsupported for unknown device")
+
+ override val icon: Drawable
+ get() = error("Unsupported for unknown device")
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BluetoothAdapterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BluetoothAdapterKosmos.kt
new file mode 100644
index 000000000000..854548c2509e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BluetoothAdapterKosmos.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 android.bluetooth.BluetoothAdapter
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.bluetoothAdapter: BluetoothAdapter by Kosmos.Fixture { mock {} }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/CachedBluetoothDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/CachedBluetoothDeviceManagerKosmos.kt
new file mode 100644
index 000000000000..07f0995f92cb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/CachedBluetoothDeviceManagerKosmos.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.CachedBluetoothDeviceManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.cachedBluetoothDeviceManager: CachedBluetoothDeviceManager by Kosmos.Fixture { mock {} }
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
new file mode 100644
index 000000000000..eef89e7dac68
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/LocalBluetoothManagerKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.LocalBluetoothManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+var Kosmos.localBluetoothManager: LocalBluetoothManager? by
+ Kosmos.Fixture {
+ mock { whenever(cachedDeviceManager).thenReturn(cachedBluetoothDeviceManager) }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
new file mode 100644
index 000000000000..5cf214a4e04a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.audioRepository by Kosmos.Fixture { FakeAudioRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
index 4788624bdf02..617fc5258ec7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
@@ -30,16 +30,14 @@ import kotlinx.coroutines.flow.update
class FakeAudioRepository : AudioRepository {
private val mutableMode = MutableStateFlow(0)
- override val mode: StateFlow<Int>
- get() = mutableMode.asStateFlow()
+ override val mode: StateFlow<Int> = mutableMode.asStateFlow()
private val mutableRingerMode = MutableStateFlow(RingerMode(0))
- override val ringerMode: StateFlow<RingerMode>
- get() = mutableRingerMode.asStateFlow()
+ override val ringerMode: StateFlow<RingerMode> = mutableRingerMode.asStateFlow()
private val mutableCommunicationDevice = MutableStateFlow<AudioDeviceInfo?>(null)
- override val communicationDevice: StateFlow<AudioDeviceInfo?>
- get() = mutableCommunicationDevice.asStateFlow()
+ override val communicationDevice: StateFlow<AudioDeviceInfo?> =
+ mutableCommunicationDevice.asStateFlow()
private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf()
private val lastAudibleVolumes: MutableMap<AudioStream, Int> = mutableMapOf()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioModeInteractorKosmos.kt
index 5e1f85c70a1b..99354be97fcb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioModeInteractorKosmos.kt
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume
+package com.android.systemui.volume.domain.interactor
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.volume.data.repository.FakeAudioRepository
+import com.android.systemui.volume.data.repository.audioRepository
-val Kosmos.audioRepository by Kosmos.Fixture { FakeAudioRepository() }
val Kosmos.audioModeInteractor by Kosmos.Fixture { AudioModeInteractor(audioRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt
new file mode 100644
index 000000000000..1b18ff5b59e2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.domain.interactor
+
+import com.android.systemui.bluetooth.bluetoothAdapter
+import com.android.systemui.bluetooth.localBluetoothManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.localMediaRepositoryFactory
+import com.android.systemui.volume.mediaOutputInteractor
+
+val Kosmos.audioOutputInteractor by
+ Kosmos.Fixture {
+ AudioOutputInteractor(
+ audioRepository,
+ audioModeInteractor,
+ testScope.backgroundScope,
+ testScope.testScheduler,
+ localBluetoothManager,
+ bluetoothAdapter,
+ deviceIconInteractor,
+ mediaOutputInteractor,
+ localMediaRepositoryFactory,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractorKosmos.kt
new file mode 100644
index 000000000000..0a27c2a2e849
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/DeviceIconInteractorKosmos.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.volume.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.deviceIconInteractor: DeviceIconInteractor by
+ Kosmos.Fixture { DeviceIconInteractor(applicationContext) }