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

Commit 7fec56e0 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Add listening to the IVolumeController for the volume changes in AudioRepository." into main

parents 0995966d 5cb6fe05
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