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

Commit 17c6b119 authored by Anton Potapov's avatar Anton Potapov
Browse files

Add VolumeController extension and collector.

Flag: com.android.systemui.use_volume_controller
Test: atest VolumeControllerCollectorTest
Test: atest AudioManagerVolumeControllerExtTest
Bug: 349348461
Change-Id: I3b4adbf4f7d15a77f22453cf8200bc2feec4f7c7
parent fe9b057b
Loading
Loading
Loading
Loading
+100 −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.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
+95 −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.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)
        }
}
+59 −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

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)
                }
            }
        }
}
+100 −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

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()
        }
}
+23 −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

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope

val Kosmos.volumeControllerCollector by
    Kosmos.Fixture { VolumeControllerCollector(applicationCoroutineScope) }