Merge "Add VolumeController extension and collector." into main
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
new file mode 100644
index 0000000..02d684d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.media.data.repository
+
+import android.media.AudioManager
+import android.media.IVolumeController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** Returns [AudioManager.setVolumeController] events as a [Flow] */
+fun AudioManager.volumeControllerEvents(): Flow<VolumeControllerEvent> =
+ callbackFlow {
+ volumeController =
+ object : IVolumeController.Stub() {
+ override fun displaySafeVolumeWarning(flags: Int) {
+ launch { send(VolumeControllerEvent.DisplaySafeVolumeWarning(flags)) }
+ }
+
+ override fun volumeChanged(streamType: Int, flags: Int) {
+ launch { send(VolumeControllerEvent.VolumeChanged(streamType, flags)) }
+ }
+
+ override fun masterMuteChanged(flags: Int) {
+ launch { send(VolumeControllerEvent.MasterMuteChanged(flags)) }
+ }
+
+ override fun setLayoutDirection(layoutDirection: Int) {
+ launch { send(VolumeControllerEvent.SetLayoutDirection(layoutDirection)) }
+ }
+
+ override fun dismiss() {
+ launch { send(VolumeControllerEvent.Dismiss) }
+ }
+
+ override fun setA11yMode(mode: Int) {
+ launch { send(VolumeControllerEvent.SetA11yMode(mode)) }
+ }
+
+ override fun displayCsdWarning(
+ csdWarning: Int,
+ displayDurationMs: Int,
+ ) {
+ launch {
+ send(
+ VolumeControllerEvent.DisplayCsdWarning(
+ csdWarning,
+ displayDurationMs,
+ )
+ )
+ }
+ }
+ }
+ awaitClose { volumeController = null }
+ }
+ .buffer()
+
+/** Models events received via [IVolumeController] */
+sealed interface VolumeControllerEvent {
+
+ /** @see [IVolumeController.displaySafeVolumeWarning] */
+ data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.volumeChanged] */
+ data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.masterMuteChanged] */
+ data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.setLayoutDirection] */
+ data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.setA11yMode] */
+ data class SetA11yMode(val mode: Int) : VolumeControllerEvent
+
+ /** @see [IVolumeController.displayCsdWarning] */
+ data class DisplayCsdWarning(
+ val csdWarning: Int,
+ val displayDurationMs: Int,
+ ) : VolumeControllerEvent
+
+ /** @see [IVolumeController.dismiss] */
+ data object Dismiss : VolumeControllerEvent
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
new file mode 100644
index 0000000..83b612d
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.media.data.repository
+
+import android.media.AudioManager
+import android.media.IVolumeController
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AudioManagerVolumeControllerExtTest {
+
+ private val testScope = TestScope()
+
+ @Captor private lateinit var volumeControllerCaptor: ArgumentCaptor<IVolumeController>
+ @Mock private lateinit var audioManager: AudioManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun displaySafeVolumeWarning_emitsEvent() =
+ testEvent(VolumeControllerEvent.DisplaySafeVolumeWarning(1)) { displaySafeVolumeWarning(1) }
+
+ @Test
+ fun volumeChanged_emitsEvent() =
+ testEvent(VolumeControllerEvent.VolumeChanged(1, 2)) { volumeChanged(1, 2) }
+
+ @Test
+ fun masterMuteChanged_emitsEvent() =
+ testEvent(VolumeControllerEvent.MasterMuteChanged(1)) { masterMuteChanged(1) }
+
+ @Test
+ fun setLayoutDirection_emitsEvent() =
+ testEvent(VolumeControllerEvent.SetLayoutDirection(1)) { setLayoutDirection(1) }
+
+ @Test
+ fun setA11yMode_emitsEvent() =
+ testEvent(VolumeControllerEvent.SetA11yMode(1)) { setA11yMode(1) }
+
+ @Test
+ fun displayCsdWarning_emitsEvent() =
+ testEvent(VolumeControllerEvent.DisplayCsdWarning(1, 2)) { displayCsdWarning(1, 2) }
+
+ @Test fun dismiss_emitsEvent() = testEvent(VolumeControllerEvent.Dismiss) { dismiss() }
+
+ private fun testEvent(
+ expectedEvent: VolumeControllerEvent,
+ emit: IVolumeController.() -> Unit,
+ ) =
+ testScope.runTest {
+ var event: VolumeControllerEvent? = null
+ audioManager.volumeControllerEvents().onEach { event = it }.launchIn(backgroundScope)
+ runCurrent()
+ verify(audioManager).volumeController = volumeControllerCaptor.capture()
+
+ volumeControllerCaptor.value.emit()
+ runCurrent()
+
+ assertThat(event).isEqualTo(expectedEvent)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
new file mode 100644
index 0000000..6859191
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
@@ -0,0 +1,59 @@
+/*
+ * 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
+
+import android.media.IVolumeController
+import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+/**
+ * This class is a bridge between
+ * [com.android.settingslib.volume.data.repository.AudioRepository.volumeControllerEvents] and the
+ * old code that uses [IVolumeController] interface directly.
+ */
+class VolumeControllerCollector
+@Inject
+constructor(@Application private val coroutineScope: CoroutineScope) {
+
+ /** Collects [Flow] of [VolumeControllerEvent] into [IVolumeController]. */
+ fun collectToController(
+ eventsFlow: Flow<VolumeControllerEvent>,
+ controller: IVolumeController
+ ) =
+ coroutineScope.launch {
+ eventsFlow.collect { event ->
+ when (event) {
+ is VolumeControllerEvent.VolumeChanged ->
+ controller.volumeChanged(event.streamType, event.flags)
+ VolumeControllerEvent.Dismiss -> controller.dismiss()
+ is VolumeControllerEvent.DisplayCsdWarning ->
+ controller.displayCsdWarning(event.csdWarning, event.displayDurationMs)
+ is VolumeControllerEvent.DisplaySafeVolumeWarning ->
+ controller.displaySafeVolumeWarning(event.flags)
+ is VolumeControllerEvent.MasterMuteChanged ->
+ controller.masterMuteChanged(event.flags)
+ is VolumeControllerEvent.SetA11yMode -> controller.setA11yMode(event.mode)
+ is VolumeControllerEvent.SetLayoutDirection ->
+ controller.setLayoutDirection(event.layoutDirection)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
new file mode 100644
index 0000000..dd78e4a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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
+
+import android.media.IVolumeController
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class VolumeControllerCollectorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val eventsFlow = MutableStateFlow<VolumeControllerEvent?>(null)
+ private val underTest = VolumeControllerCollector(kosmos.applicationCoroutineScope)
+
+ private val volumeController = mock<IVolumeController> {}
+
+ @Test
+ fun volumeControllerEvent_volumeChanged_callsMethod() =
+ testEvent(VolumeControllerEvent.VolumeChanged(3, 0)) {
+ verify(volumeController) { 1 * { volumeController.volumeChanged(eq(3), eq(0)) } }
+ }
+
+ @Test
+ fun volumeControllerEvent_dismiss_callsMethod() =
+ testEvent(VolumeControllerEvent.Dismiss) {
+ verify(volumeController) { 1 * { volumeController.dismiss() } }
+ }
+
+ @Test
+ fun volumeControllerEvent_displayCsdWarning_callsMethod() =
+ testEvent(VolumeControllerEvent.DisplayCsdWarning(0, 1)) {
+ verify(volumeController) { 1 * { volumeController.displayCsdWarning(eq(0), eq(1)) } }
+ }
+
+ @Test
+ fun volumeControllerEvent_displaySafeVolumeWarning_callsMethod() =
+ testEvent(VolumeControllerEvent.DisplaySafeVolumeWarning(1)) {
+ verify(volumeController) { 1 * { volumeController.displaySafeVolumeWarning(eq(1)) } }
+ }
+
+ @Test
+ fun volumeControllerEvent_masterMuteChanged_callsMethod() =
+ testEvent(VolumeControllerEvent.MasterMuteChanged(1)) {
+ verify(volumeController) { 1 * { volumeController.masterMuteChanged(1) } }
+ }
+
+ @Test
+ fun volumeControllerEvent_setA11yMode_callsMethod() =
+ testEvent(VolumeControllerEvent.SetA11yMode(1)) {
+ verify(volumeController) { 1 * { volumeController.setA11yMode(1) } }
+ }
+
+ @Test
+ fun volumeControllerEvent_SetLayoutDirection_callsMethod() =
+ testEvent(VolumeControllerEvent.SetLayoutDirection(1)) {
+ verify(volumeController) { 1 * { volumeController.setLayoutDirection(eq(1)) } }
+ }
+
+ private fun testEvent(event: VolumeControllerEvent, verify: () -> Unit) =
+ kosmos.testScope.runTest {
+ underTest.collectToController(eventsFlow.filterNotNull(), volumeController)
+
+ eventsFlow.value = event
+ runCurrent()
+
+ verify()
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt
new file mode 100644
index 0000000..d60f14c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.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
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.volumeControllerCollector by
+ Kosmos.Fixture { VolumeControllerCollector(applicationCoroutineScope) }