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

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

Merge "Add remote media session functionality to LocalMediaRepository" into main

parents fd3aa42f db91ca37
Loading
Loading
Loading
Loading
+26 −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.volume.data.model

import android.media.RoutingSessionInfo

/** Models a routing session which is created when a media route is selected. */
data class RoutingSession(
    val routingSessionInfo: RoutingSessionInfo,
    val isVolumeSeekBarEnabled: Boolean,
    val isMediaOutputDisabled: Boolean,
)
+5 −48
Original line number Diff line number Diff line
@@ -16,15 +16,12 @@

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

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
@@ -32,7 +29,6 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@@ -40,7 +36,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -77,7 +72,7 @@ interface AudioRepository {
}

class AudioRepositoryImpl(
    private val context: Context,
    private val audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
    private val audioManager: AudioManager,
    private val backgroundCoroutineContext: CoroutineContext,
    private val coroutineScope: CoroutineScope,
@@ -93,30 +88,9 @@ class AudioRepositoryImpl(
            .flowOn(backgroundCoroutineContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)

    private val audioManagerIntents: SharedFlow<String> =
        callbackFlow {
                val receiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent) {
                            intent.action?.let { action -> launch { send(action) } }
                        }
                    }
                context.registerReceiver(
                    receiver,
                    IntentFilter().apply {
                        for (action in allActions) {
                            addAction(action)
                        }
                    }
                )

                awaitClose { context.unregisterReceiver(receiver) }
            }
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed())

    override val ringerMode: StateFlow<RingerMode> =
        audioManagerIntents
            .filter { ringerActions.contains(it) }
        audioManagerIntentsReceiver.intents
            .filter { AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION == it.action }
            .map { RingerMode(audioManager.ringerModeInternal) }
            .flowOn(backgroundCoroutineContext)
            .stateIn(
@@ -146,8 +120,7 @@ class AudioRepositoryImpl(
                )

    override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
        return audioManagerIntents
            .filter { modelActions.contains(it) }
        return audioManagerIntentsReceiver.intents
            .map { getCurrentAudioStream(audioStream) }
            .flowOn(backgroundCoroutineContext)
    }
@@ -189,20 +162,4 @@ class AudioRepositoryImpl(
            // return STREAM_VOICE_CALL in getAudioStream
            audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
        }

    private companion object {
        val modelActions =
            setOf(
                AudioManager.STREAM_MUTE_CHANGED_ACTION,
                AudioManager.MASTER_MUTE_CHANGED_ACTION,
                AudioManager.VOLUME_CHANGED_ACTION,
                AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
                AudioManager.STREAM_DEVICES_CHANGED_ACTION,
            )
        val ringerActions =
            setOf(
                AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
            )
        val allActions = ringerActions + modelActions
    }
}
+71 −25
Original line number Diff line number Diff line
@@ -15,8 +15,13 @@
 */
package com.android.settingslib.volume.data.repository

import android.media.AudioManager
import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
@@ -24,10 +29,15 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

/** Repository providing data about connected media devices. */
interface LocalMediaRepository {
@@ -37,15 +47,26 @@ interface LocalMediaRepository {

    /** Currently connected media device */
    val currentConnectedDevice: StateFlow<MediaDevice?>

    val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>

    suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
}

class LocalMediaRepositoryImpl(
    audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
    private val localMediaManager: LocalMediaManager,
    private val mediaRouter2Manager: MediaRouter2Manager,
    coroutineScope: CoroutineScope,
    backgroundContext: CoroutineContext,
    private val backgroundContext: CoroutineContext,
) : LocalMediaRepository {

    private val deviceUpdates: Flow<DevicesUpdate> = callbackFlow {
    private val devicesChanges =
        audioManagerIntentsReceiver.intents.filter {
            AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
        }
    private val mediaDevicesUpdates: Flow<DevicesUpdate> =
        callbackFlow {
                val callback =
                    object : LocalMediaManager.DeviceCallback {
                        override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) {
@@ -71,9 +92,10 @@ class LocalMediaRepositoryImpl(
                    localMediaManager.unregisterCallback(callback)
                }
            }
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)

    override val mediaDevices: StateFlow<Collection<MediaDevice>> =
        deviceUpdates
        mediaDevicesUpdates
            .mapNotNull {
                if (it is DevicesUpdate.DeviceListUpdate) {
                    it.newDevices ?: emptyList()
@@ -85,7 +107,7 @@ class LocalMediaRepositoryImpl(
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    override val currentConnectedDevice: StateFlow<MediaDevice?> =
        deviceUpdates
        merge(devicesChanges, mediaDevicesUpdates)
            .map { localMediaManager.currentConnectedDevice }
            .stateIn(
                coroutineScope,
@@ -93,6 +115,30 @@ class LocalMediaRepositoryImpl(
                localMediaManager.currentConnectedDevice
            )

    override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
        merge(devicesChanges, mediaDevicesUpdates)
            .onStart { emit(Unit) }
            .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
        withContext(backgroundContext) {
            if (sessionId == null) {
                localMediaManager.adjustSessionVolume(volume)
            } else {
                localMediaManager.adjustSessionVolume(sessionId, volume)
            }
        }
    }

    private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
        RoutingSession(
            info,
            isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
            isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
        )

    private sealed interface DevicesUpdate {

        data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
+15 −37
Original line number Diff line number Diff line
@@ -16,30 +16,23 @@

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

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.headsetAudioModeChanges
import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/** Provides controllers for currently active device media sessions. */
interface MediaControllerRepository {
@@ -49,40 +42,25 @@ interface MediaControllerRepository {
}

class MediaControllerRepositoryImpl(
    private val context: Context,
    audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
    private val mediaSessionManager: MediaSessionManager,
    localBluetoothManager: LocalBluetoothManager?,
    coroutineScope: CoroutineScope,
    backgroundContext: CoroutineContext,
) : MediaControllerRepository {

    private val devicesChanges: Flow<Unit> =
        callbackFlow {
                val receiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent?) {
                            if (AudioManager.STREAM_DEVICES_CHANGED_ACTION == intent?.action) {
                                launch { send(Unit) }
    private val devicesChanges =
        audioManagerIntentsReceiver.intents.filter {
            AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
        }
                        }
                    }
                context.registerReceiver(
                    receiver,
                    IntentFilter(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
                )

                awaitClose { context.unregisterReceiver(receiver) }
            }
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)

    override val activeMediaController: StateFlow<MediaController?> =
        combine(
                localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
                    ?: emptyFlow(),
                devicesChanges.onStart { emit(Unit) },
            ) { _, _ ->
                getActiveLocalMediaController()
        buildList {
                localBluetoothManager?.headsetAudioModeChanges?.let { add(it) }
                add(devicesChanges)
            }
            .merge()
            .onStart { emit(Unit) }
            .map { getActiveLocalMediaController() }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)

+57 −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.volume.domain.interactor

import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.domain.model.RoutingSession
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

class LocalMediaInteractor(
    private val repository: LocalMediaRepository,
    coroutineScope: CoroutineScope,
) {

    /** Available devices list */
    val mediaDevices: StateFlow<Collection<MediaDevice>>
        get() = repository.mediaDevices

    /** Currently connected media device */
    val currentConnectedDevice: StateFlow<MediaDevice?>
        get() = repository.currentConnectedDevice

    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
        repository.remoteRoutingSessions
            .map { sessions ->
                sessions.map {
                    RoutingSession(
                        routingSessionInfo = it.routingSessionInfo,
                        isMediaOutputDisabled = it.isMediaOutputDisabled,
                        isVolumeSeekBarEnabled =
                            it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
                    )
                }
            }
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
        repository.adjustSessionVolume(sessionId, volume)
}
Loading