Android上声音输出设备之间的无缝及时切换是一个通常被认为是理所当然的功能,但缺少它(或它的问题)是非常烦人的。今天我们就来分析下Android铃声如何实现这样的切换,从用户手动切换到耳机连接时自动切换。同时,让我们谈谈在通话期间暂停音频系统的其余部分。此实现适用于几乎所有调用应用程序,因为它在系统级别而不是调用引擎级别(例如 WebRTC)运行。
音频输出设备管理
Android 声音输出设备的所有管理都是通过系统的AudioManager
要使用它,您需要添加以下权限AndroidManifest.xml
:
首先,当我们的应用程序开始通话时,强烈建议捕获音频焦点——让系统知道用户现在正在与某人通信,并且最好不要被其他应用程序的声音分散注意力。例如,如果用户正在听音乐,但接到电话并接听了电话——音乐将在通话期间暂停。
音频焦点请求有两种机制——旧的已弃用,新的从 Android 8.0 开始可用。我们为系统的所有版本实施:
// Receiving an AudioManager sample
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// We need a "request" for the new approach. Let's generate it for versions >=8.0 and leave null for older ones
@RequiresApi(Build.VERSION_CODES.O)
private fun getAudioFocusRequest() =
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).build()
// Focus request
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Use the generated request
audioManager.requestAudioFocus(getAudioFocusRequest())
} else {
audioManager.requestAudioFocus(
// Listener of receiving focus. Let's leave it empty for the sake of simpleness
{ },
// Requesting a call focus
AudioAttributes.CONTENT_TYPE_SPEECH,
AudioManager.AUDIOFOCUS_GAIN
)
}
指定最合适的音量非常重要,ContentType
并且Usage
基于这些,系统确定要使用的自定义音量设置(媒体音量或铃声音量)以及如何处理其他音频源(静音、暂停或允许像以前一样运行)。
val savedAudioMode = audioManager.mode
val savedIsSpeakerOn = audioManager.isSpeakerphoneOn
val savedIsMicrophoneMuted = audioManager.isMicrophoneMute
太好了,我们有音频焦点。强烈建议在更改任何内容之前立即保存原始 AudioManager 设置 - 这将允许我们在通话结束时将其恢复到之前的状态。您应该同意,如果一个应用程序的音量控制会影响所有其他应用程序,那将是非常不方便的
现在我们可以开始设置我们的默认值了。这可能取决于呼叫的类型(通常音频呼叫在“免提电话”上,视频呼叫在“免提电话”上)、应用程序中的用户设置或仅取决于上次使用的免提电话。我们的条件应用程序是一个视频应用程序,因此我们将立即设置免提电话:
// Moving AudioManager to the "call" state
audioManager.mode = AudioSystem.MODE_IN_COMMUNICATION
// Enabling speakerphone
audioManager.isSpeakerphoneOn = true
太好了,我们已应用默认设置。如果应用程序设计提供了一个按钮来切换扬声器,我们现在可以很容易地实现它的处理:
audioManager.isSpeakerphoneOn = !audioManager.isSpeakerphoneOn
监听耳机的连接
我们已经学会了如何实现免提切换,但是如果连接耳机会发生什么?没什么,因为audioManager.isSpeakerphoneOn还在true!当然,用户希望插入耳机后,声音会开始通过它们播放。反之亦然——如果我们有视频通话,那么当我们断开耳机时,声音应该开始通过扬声器播放。
没有出路,我们只好监听耳机的连接。让我马上告诉你,有线和蓝牙耳机的连接跟踪是不同的,所以我们必须同时实现两种机制。让我们从有线的开始,将逻辑放在一个单独的类中:
class HeadsetStateProvider(
private val context: Context,
private val audioManager: AudioManager
) {
// The current state of wired headies; true means enabled
val isHeadsetPlugged = MutableStateFlow(getHeadsetState())
// Create BroadcastReceiver to track the headset connection and disconnection events
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
when (intent.getIntExtra("state", -1)) {
// 0 -- the headset is offline, 1 -- the headset is online
0 -> isHeadsetPlugged.value = false
1 -> isHeadsetPlugged.value = true
}
}
}
}
init {
val filter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
// Register our BroadcastReceiver
context.registerReceiver(receiver, filter)
}
// The method to receive a current headset state. It's used to initialize the starting point.
fun getHeadsetState(): Boolean {
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
return audioDevices.any {
it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET
}
}
}
在我们的示例中,我们使用StateFlow来实现订阅连接状态,但是,我们可以实现,例如,HeadsetStateProviderListener
现在只需初始化这个类并观察该isHeadsetPlugged字段,当它改变时打开或关闭扬声器:
headsetStateProvider.isHeadsetPlugged
// If the headset isn't on, speakerphone is.
.onEach { audioManager.isSpeakerphoneOn = !it }
.launchIn(someCoroutineScope)
蓝牙耳机连接监控
现在我们对蓝牙耳机等安卓声音输出设备实现同样的监听机制:
class BluetoothHeadsetStateProvider(
private val context: Context,
private val bluetoothManager: BluetoothManager
) {
val isHeadsetConnected = MutableStateFlow(getHeadsetState())
init {
// Receive the adapter from BluetoothManager and install our ServiceListener
bluetoothManager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
// This method will be used when the new device connects
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
// Checking if it is the headset that's active
if (profile == BluetoothProfile.HEADSET)
// Refreshing state
isHeadsetConnected.value = true
}
// This method will be used when the new device disconnects
override fun onServiceDisconnected(profile: Int)
if (profile == BluetoothProfile.HEADSET)
isHeadsetConnected.value = false
}
// Enabling ServiceListener for headsets
}, BluetoothProfile.HEADSET)
}
// The method of receiving the current state of the bluetooth headset. Only used to initialize the starting state
private fun getHeadsetState(): Boolean {
val adapter = bluetoothManager.adapter
// Checking if there are active headsets
return adapter?.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED
}
}
要使用蓝牙,我们需要另一个解决方案:
现在在未连接耳机时自动打开免提电话,反之亦然,当连接新耳机时:
combine(headsetStateProvider.isHeadsetPlugged, bluetoothHeadsetStateProvider.isHeadsetPlugged) { connected, bluetoothConnected ->
audioManager.isSpeakerphoneOn = !connected && !bluetoothConnected
}
.launchIn(someCoroutineScope)
整理
通话结束后,音频焦点对我们不再有用,我们必须摆脱它。让我们恢复一开始保存的设置:
audioManager.mode = savedAudioMode
audioManager.isMicrophoneMute = savedIsMicrophoneMuted
audioManager.isSpeakerphoneOn = savedIsSpeakerOn
现在,实际上,让我们放弃焦点。同样,实现取决于系统版本:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioManager.abandonAudioFocusRequest(getAudioFocusRequest())
} else {
// Listener для простоты опять оставим пустым
audioManager.abandonAudioFocus { }
}
最后
太好了,我们已经在我们的应用程序中实现了在 Android 声音输出设备之间切换的完美 UX。这种方法的主要优点是它几乎独立于调用的具体实现:在任何情况下,播放的音频都将由“AudioManager”控制,我们完全控制在它的级别!
链接:https://forasoft.hashnode.dev/how-to-implement-audio-output-switching-during-the-call-on-android-app