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

Commit 5cb6fe05 authored by Anton Potapov's avatar Anton Potapov
Browse files

Add listening to the IVolumeController for the volume changes in AudioRepository.

Flag: com.android.systemui.use_volume_controller
Test: manual on the phone. change volume using both: volume panel and volume dialog
Test: atest AudioRepositoryTest
Test: atest VolumeDialogControllerImplTestKt
Bug: 349348461
Change-Id: Ic8d9095808f890b21c4de3fd4cdd6db131d3954d
parent 78e5497b
Loading
Loading
Loading
Loading
+47 −0
Original line number Diff line number Diff line
@@ -14,62 +14,9 @@
 * limitations under the License.
 */

package com.android.settingslib.media.data.repository
package com.android.settingslib.volume.data.model

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 {
+101 −22
Original line number Diff line number Diff line
@@ -22,9 +22,12 @@ import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.AudioDeviceCategory
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import android.media.IVolumeController
import android.provider.Settings
import android.util.Log
import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.settingslib.volume.shared.AudioLogger
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
@@ -36,10 +39,13 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
@@ -73,6 +79,11 @@ interface AudioRepository {
     */
    val communicationDevice: StateFlow<AudioDeviceInfo?>

    /** Events from [AudioManager.setVolumeController] */
    val volumeControllerEvents: Flow<VolumeControllerEvent>

    fun init()

    /** State of the [AudioStream]. */
    fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel>

@@ -90,8 +101,9 @@ interface AudioRepository {
    suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)

    /** Gets audio device category. */
    @AudioDeviceCategory
    suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int
    @AudioDeviceCategory suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int

    suspend fun notifyVolumeControllerVisible(isVisible: Boolean)
}

class AudioRepositoryImpl(
@@ -101,8 +113,10 @@ class AudioRepositoryImpl(
    private val backgroundCoroutineContext: CoroutineContext,
    private val coroutineScope: CoroutineScope,
    private val logger: AudioLogger,
    shouldUseVolumeController: Boolean,
) : AudioRepository {

    private val volumeController = ProducingVolumeController()
    private val streamSettingNames: Map<AudioStream, String> =
        mapOf(
            AudioStream(AudioManager.STREAM_VOICE_CALL) to Settings.System.VOLUME_VOICE,
@@ -116,6 +130,13 @@ class AudioRepositoryImpl(
            AudioStream(AudioManager.STREAM_ASSISTANT) to Settings.System.VOLUME_ASSISTANT,
        )

    override val volumeControllerEvents: Flow<VolumeControllerEvent> =
        if (shouldUseVolumeController) {
            volumeController.events
        } else {
            emptyFlow()
        }

    override val mode: StateFlow<Int> =
        callbackFlow {
                val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) }
@@ -159,6 +180,14 @@ class AudioRepositoryImpl(
                    audioManager.communicationDevice,
                )

    override fun init() {
        try {
            audioManager.volumeController = volumeController
        } catch (error: SecurityException) {
            Log.wtf("AudioManager", "Unable to set the volume controller", error)
        }
    }

    override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
        return merge(
                audioManagerEventsReceiver.events.filter {
@@ -169,10 +198,12 @@ class AudioRepositoryImpl(
                    }
                },
                volumeSettingChanges(audioStream),
                volumeControllerEvents.filter { it is VolumeControllerEvent.VolumeChanged },
            )
            .conflate()
            .map { getCurrentAudioStream(audioStream) }
            .onStart { emit(getCurrentAudioStream(audioStream)) }
            .distinctUntilChanged()
            .onEach { logger.onVolumeUpdateReceived(audioStream, it) }
            .flowOn(backgroundCoroutineContext)
    }
@@ -228,6 +259,12 @@ class AudioRepositoryImpl(
        }
    }

    override suspend fun notifyVolumeControllerVisible(isVisible: Boolean) {
        withContext(backgroundCoroutineContext) {
            audioManager.notifyVolumeControllerVisible(volumeController, isVisible)
        }
    }

    private fun getMinVolume(stream: AudioStream): Int =
        try {
            audioManager.getStreamMinVolume(stream.value)
@@ -253,3 +290,45 @@ class AudioRepositoryImpl(
        }
    }
}

private class ProducingVolumeController : IVolumeController.Stub() {

    private val mutableEvents = MutableSharedFlow<VolumeControllerEvent>(extraBufferCapacity = 32)
    val events = mutableEvents.asSharedFlow()

    override fun displaySafeVolumeWarning(flags: Int) {
        mutableEvents.tryEmit(VolumeControllerEvent.DisplaySafeVolumeWarning(flags))
    }

    override fun volumeChanged(streamType: Int, flags: Int) {
        mutableEvents.tryEmit(VolumeControllerEvent.VolumeChanged(streamType, flags))
    }

    override fun masterMuteChanged(flags: Int) {
        mutableEvents.tryEmit(VolumeControllerEvent.MasterMuteChanged(flags))
    }

    override fun setLayoutDirection(layoutDirection: Int) {
        mutableEvents.tryEmit(VolumeControllerEvent.SetLayoutDirection(layoutDirection))
    }

    override fun dismiss() {
        mutableEvents.tryEmit(VolumeControllerEvent.Dismiss)
    }

    override fun setA11yMode(mode: Int) {
        mutableEvents.tryEmit(VolumeControllerEvent.SetA11yMode(mode))
    }

    override fun displayCsdWarning(
        csdWarning: Int,
        displayDurationMs: Int,
    ) {
        mutableEvents.tryEmit(
            VolumeControllerEvent.DisplayCsdWarning(
                csdWarning,
                displayDurationMs,
            )
        )
    }
}
+25 −2
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -111,6 +112,7 @@ class AudioRepositoryTest {
                testScope.testScheduler,
                testScope.backgroundScope,
                logger,
                true,
            )
    }

@@ -261,8 +263,8 @@ class AudioRepositoryTest {
    @Test
    fun getBluetoothAudioDeviceCategory() {
        testScope.runTest {
            `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78")).thenReturn(
                AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)
            `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78"))
                .thenReturn(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES)

            val category = underTest.getBluetoothAudioDeviceCategory("12:34:56:78")
            runCurrent()
@@ -271,6 +273,27 @@ class AudioRepositoryTest {
        }
    }

    @Test
    fun useVolumeControllerDisabled_setVolumeController_notCalled() {
        testScope.runTest {
            underTest =
                AudioRepositoryImpl(
                    eventsReceiver,
                    audioManager,
                    contentResolver,
                    testScope.testScheduler,
                    testScope.backgroundScope,
                    logger,
                    false,
                )

            underTest.volumeControllerEvents.launchIn(backgroundScope)
            runCurrent()

            verify(audioManager, never()).volumeController = any()
        }
    }

    private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
        verify(audioManager)
            .addOnCommunicationDeviceChangedListener(
+22 −3
Original line number Diff line number Diff line
@@ -14,12 +14,15 @@
 * limitations under the License.
 */

package com.android.settingslib.media.data.repository
package com.android.settingslib.volume.data.repository

import android.content.ContentResolver
import android.media.AudioManager
import android.media.IVolumeController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -39,16 +42,32 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AudioManagerVolumeControllerExtTest {
class AudioRepositoryVolumeControllerEventsTest {

    private val testScope = TestScope()

    @Captor private lateinit var volumeControllerCaptor: ArgumentCaptor<IVolumeController>
    @Mock private lateinit var audioManager: AudioManager
    @Mock private lateinit var contentResolver: ContentResolver

    private val logger = FakeAudioRepositoryLogger()
    private val eventsReceiver = FakeAudioManagerEventsReceiver()

    private lateinit var underTest: AudioRepository

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        underTest =
            AudioRepositoryImpl(
                eventsReceiver,
                audioManager,
                contentResolver,
                testScope.testScheduler,
                testScope.backgroundScope,
                logger,
                true,
            )
    }

    @Test
@@ -83,7 +102,7 @@ class AudioManagerVolumeControllerExtTest {
    ) =
        testScope.runTest {
            var event: VolumeControllerEvent? = null
            audioManager.volumeControllerEvents().onEach { event = it }.launchIn(backgroundScope)
            underTest.volumeControllerEvents.onEach { event = it }.launchIn(backgroundScope)
            runCurrent()
            verify(audioManager).volumeController = volumeControllerCaptor.capture()

+14 −8
Original line number Diff line number Diff line
@@ -17,7 +17,8 @@
package com.android.systemui.volume

import android.media.IVolumeController
import com.android.settingslib.media.data.repository.VolumeControllerEvent
import com.android.settingslib.volume.data.model.VolumeControllerEvent
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -29,17 +30,17 @@ import kotlinx.coroutines.launch
 * [com.android.settingslib.volume.data.repository.AudioRepository.volumeControllerEvents] and the
 * old code that uses [IVolumeController] interface directly.
 */
class VolumeControllerCollector
class VolumeControllerAdapter
@Inject
constructor(@Application private val coroutineScope: CoroutineScope) {
constructor(
    @Application private val coroutineScope: CoroutineScope,
    private val audioRepository: AudioRepository,
) {

    /** Collects [Flow] of [VolumeControllerEvent] into [IVolumeController]. */
    fun collectToController(
        eventsFlow: Flow<VolumeControllerEvent>,
        controller: IVolumeController
    ) =
    fun collectToController(controller: IVolumeController) {
        coroutineScope.launch {
            eventsFlow.collect { event ->
            audioRepository.volumeControllerEvents.collect { event ->
                when (event) {
                    is VolumeControllerEvent.VolumeChanged ->
                        controller.volumeChanged(event.streamType, event.flags)
@@ -57,3 +58,8 @@ constructor(@Application private val coroutineScope: CoroutineScope) {
            }
        }
    }

    fun notifyVolumeControllerVisible(isVisible: Boolean) {
        coroutineScope.launch { audioRepository.notifyVolumeControllerVisible(isVisible) }
    }
}
Loading