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

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

Merge "Add audio stream managing functionality to AudioRepository." into main

parents 22c9a468 bdd839c9
Loading
Loading
Loading
Loading
+158 −3
Original line number Diff line number Diff line
@@ -16,29 +16,71 @@

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.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
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
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

/** Provides audio managing functionality and data. */
/** Provides audio streams state and managing functionality. */
interface AudioRepository {

    /** Current [AudioManager.getMode]. */
    val mode: StateFlow<Int>

    /**
     * Ringtone mode.
     *
     * @see AudioManager.getRingerModeInternal
     */
    val ringerMode: StateFlow<RingerMode>

    /**
     * Communication device. Emits null when there is no communication device available.
     *
     * @see AudioDeviceInfo.getType
     */
    val communicationDevice: StateFlow<AudioDeviceInfo?>

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

    /** Current state of the [AudioStream]. */
    suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel

    suspend fun setVolume(audioStream: AudioStream, volume: Int)

    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean)
}

class AudioRepositoryImpl(
    private val context: Context,
    private val audioManager: AudioManager,
    backgroundCoroutineContext: CoroutineContext,
    coroutineScope: CoroutineScope,
    private val backgroundCoroutineContext: CoroutineContext,
    private val coroutineScope: CoroutineScope,
) : AudioRepository {

    override val mode: StateFlow<Int> =
@@ -50,4 +92,117 @@ 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) }
            .map { RingerMode(audioManager.ringerModeInternal) }
            .flowOn(backgroundCoroutineContext)
            .stateIn(
                coroutineScope,
                SharingStarted.WhileSubscribed(),
                RingerMode(audioManager.ringerModeInternal),
            )

    override val communicationDevice: StateFlow<AudioDeviceInfo?>
        get() =
            callbackFlow {
                    val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
                    audioManager.addOnCommunicationDeviceChangedListener(
                        DirectExecutor.INSTANCE,
                        listener
                    )

                    awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
                }
                .filterNotNull()
                .map { audioManager.communicationDevice }
                .flowOn(backgroundCoroutineContext)
                .stateIn(
                    coroutineScope,
                    SharingStarted.WhileSubscribed(),
                    audioManager.communicationDevice,
                )

    override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
        return audioManagerIntents
            .filter { modelActions.contains(it) }
            .map { getCurrentAudioStream(audioStream) }
            .flowOn(backgroundCoroutineContext)
    }

    override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel {
        return withContext(backgroundCoroutineContext) {
            AudioStreamModel(
                audioStream = audioStream,
                minVolume = getMinVolume(audioStream),
                maxVolume = audioManager.getStreamMaxVolume(audioStream.value),
                volume = audioManager.getStreamVolume(audioStream.value),
                isAffectedByRingerMode =
                    audioManager.isStreamAffectedByRingerMode(audioStream.value),
                isMuted = audioManager.isStreamMute(audioStream.value)
            )
        }
    }

    override suspend fun setVolume(audioStream: AudioStream, volume: Int) =
        withContext(backgroundCoroutineContext) {
            audioManager.setStreamVolume(audioStream.value, volume, 0)
        }

    override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) =
        withContext(backgroundCoroutineContext) {
            if (isMuted) {
                audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_MUTE)
            } else {
                audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_UNMUTE)
            }
        }

    private fun getMinVolume(stream: AudioStream): Int =
        try {
            audioManager.getStreamMinVolume(stream.value)
        } catch (e: IllegalArgumentException) {
            // Fallback to STREAM_VOICE_CALL because
            // CallVolumePreferenceController.java default
            // 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
    }
}
+32 −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.repository

import android.content.Context
import android.media.AudioSystem

/** Provides the current state of the audio system. */
interface AudioSystemRepository {

    val isSingleVolume: Boolean
}

class AudioSystemRepositoryImpl(private val context: Context) : AudioSystemRepository {

    override val isSingleVolume: Boolean
        get() = AudioSystem.isSingleVolume(context)
}
+45 −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.shared.model

import android.media.AudioManager

/** Type-safe wrapper for [AudioManager] audio stream. */
@JvmInline
value class AudioStream(val value: Int) {
    init {
        require(value in supportedStreamTypes) { "Unsupported stream=$value" }
    }

    private companion object {
        val supportedStreamTypes =
            setOf(
                AudioManager.STREAM_VOICE_CALL,
                AudioManager.STREAM_SYSTEM,
                AudioManager.STREAM_RING,
                AudioManager.STREAM_MUSIC,
                AudioManager.STREAM_ALARM,
                AudioManager.STREAM_NOTIFICATION,
                AudioManager.STREAM_BLUETOOTH_SCO,
                AudioManager.STREAM_SYSTEM_ENFORCED,
                AudioManager.STREAM_DTMF,
                AudioManager.STREAM_TTS,
                AudioManager.STREAM_ACCESSIBILITY,
                AudioManager.STREAM_ASSISTANT,
            )
    }
}
+27 −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.shared.model

/** Current state of the audio stream. */
data class AudioStreamModel(
    val audioStream: AudioStream,
    val volume: Int,
    val minVolume: Int,
    val maxVolume: Int,
    val isAffectedByRingerMode: Boolean,
    val isMuted: Boolean,
)
+38 −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.shared.model

import android.media.AudioManager

/** Type-safe wrapper for [AudioManager] ringer mode. */
@JvmInline
value class RingerMode(val value: Int) {

    init {
        require(value in supportedRingerModes) { "Unsupported stream=$value" }
    }

    private companion object {
        val supportedRingerModes =
            setOf(
                AudioManager.RINGER_MODE_SILENT,
                AudioManager.RINGER_MODE_VIBRATE,
                AudioManager.RINGER_MODE_NORMAL,
                AudioManager.RINGER_MODE_MAX,
            )
    }
}
Loading