How to Implement Audio Output Switching During the Call on Android App?
17.2.2022
·
Обновлено
8.30.2024
Seamless and timely switching between the sound output devices on Android is a feature that is usually taken for granted, but the lack of it (or problems with it) is very annoying. Today we will analyze how to implement such switching in Android ringtones, starting from the manual switching by the user to the automatic switching when headsets are connected. At the same time, let’s talk about pausing the rest of the audio system for the duration of the call. This implementation is suitable for almost all calling applications since it operates at the system level rather than the call engine level, e.g., WebRTC.
Audio output device management
All management of Android sound output devices is implemented through the system’s `AudioManager`. To work with it you need to add permission to `AndroidManifest.xml`:
First of all, when a call starts in our app, it is highly recommended to capture the audio focus — let the system know that the user is now communicating with someone, and it is best not to be distracted by sounds from other apps. For example, if the user was listening to music, but received a call and answered — the music will be paused for the duration of the call.
There are two mechanisms of audio focus request — the old one is deprecated, and the new one is available since Android 8.0. We implement for all versions of the system:
// 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
)
}
It is important to specify the most appropriate `ContentType` and `Usage` — based on these, the system determines which of the custom volume settings to use (media volume or ringer volume) and what to do with the other audio sources (mute, pause, or allow to run as before).
val savedAudioMode = audioManager.modeval savedIsSpeakerOn = audioManager.isSpeakerphoneOnval savedIsMicrophoneMuted = audioManager.isMicrophoneMute
Great, we’ve got audio focus. It is highly recommended to save the original AudioManager settings right away before changing anything – this will allow us to restore it to its previous state when the call is over. You should agree that it would be very inconvenient if one application’s volume control would affect all the others
Now we can start setting our defaults. It may depend on the type of call (usually audio calls are on “speakerphone” and video calls are on “speakerphone”), on the user settings in the application or just on the last used speakerphone. Our conditional app is a video app, so we’ll set up the speakerphone right away:
// Moving AudioManager to the "call" state
audioManager.mode = AudioSystem.MODE_IN_COMMUNICATION
// Enabling speakerphone
audioManager.isSpeakerphoneOn = true
Great, we have applied the default settings. If the application design provides a button to toggle the speakerphone, we can now very easily implement its handling:
We’ve learned how to implement hands-free switching, but what happens if you connect headphones? Nothing, because `audioManager.isSpeakerphoneOn` is still `true`! And the user, of course, expects that when headphones are plugged in, the sound will start playing through them. And vice versa — if we have a video call, then when we disconnect the headphones the sound should start playing through the speakerphone.
There is no way out, we have to monitor the connection of the headphones. Let me tell you right away, the connection of wired and Bluetooth headphones is tracked differently, so we have to implement two mechanisms at once. Let’s start with wired ones and put the logic in a separate class:
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
}
}
}
In our example, we use `StateFlow` to implement subscription to the connection state, but instead, we can implement, for example, `HeadsetStateProviderListener`
Now just initialize this class and observe the `isHeadsetPlugged` field, turning the speaker on or off when it changes:
headsetStateProvider.isHeadsetPlugged
// If the headset isn't on, speakerphone is.
.onEach { audioManager.isSpeakerphoneOn = !it }
.launchIn(someCoroutineScope)
Bluetooth headphones connection monitoring
Now we implement the same monitoring mechanism for such Android sound output devices as Bluetooth headphones:
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)
// Обновляем состояние
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
}
}
Now we implement the same monitoring mechanism for Bluetooth headphones:
To work with Bluetooth, we need another permission. For Android 12 and above, you need to declare in the manifest file and request at runtime following permission:
And now let’s give away the focus. Again, the implementation depends on the system version:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioManager.abandonAudioFocusRequest(getAudioFocusRequest())
} else {
// Let's leave it empty to keep it simple
audioManager.abandonAudioFocus { }
}
Limitations
In the app you can switch the sound output between three device types:
speaker
earpiece or wired
Bluetooth device
However you cannot switch between two Bluetooth devices. On Android 11 though, there’s now a feature to add the device switch to Notification. The switcher displays all available devices with the enabled volume control feature. So it will simply not show users the devices they can’t switch to from the one they’re currently using as an output.
To add the switcher, use the notif with the Notification.MediaStyle style with MediaSession connected to it:
val mediaSession = MediaSession(this, MEDIA_SESSION_TAG)
val style = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken)
val notification = Notification.Builder(this, CHANNEL_ID)
.setStyle(style)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.build()
But how does Spotify have that quick and easy device switcher?
Our reader has noticed that Spotify does have that feature where you can switch between any devices you need. We cannot know for sure how they do that. But what we assume is that most likely Spotify implemented audio devices switching with MediaRouter API. It is used for seamless data exchange between two devices.
Great, here we have implemented the perfect UX of switching between Android sound output devices in our app. The main advantage of this approach is that it is almost independent of the specific implementation of calls: in any case, the played audio will be controlled by `AudioManager’, and we control exactly at its level!
Cообщение не отправлено, что-то пошло не так при отправке формы. Попробуйте еще раз.
e-learning-software-development-how-to
Jayempire
9.10.2024
Cool
simulate-slow-network-connection-57
Samrat Rajput
27.7.2024
The Redmi 9 Power boasts a 6000mAh battery, an AI quad-camera setup with a 48MP primary sensor, and a 6.53-inch FHD+ display. It is powered by a Qualcomm Snapdragon 662 processor, offering a balance of performance and efficiency. The phone also features a modern design with a textured back and is available in multiple color options.
this is defenetely what i was looking for. thanks!
how-to-implement-screen-sharing-in-ios-1193
liza
25.1.2024
Can you please provide example for flutter as well . I'm having issue to screen share in IOS flutter.
guide-to-software-estimating-95
Nikolay Sapunov
10.1.2024
Thank you Joy! Glad to be helpful :)
guide-to-software-estimating-95
Joy Gomez
10.1.2024
I stumbled upon this guide from Fora Soft while looking for insights into making estimates for software development projects, and it didn't disappoint. The step-by-step breakdown and the inclusion of best practices make it a valuable resource. I'm already seeing positive changes in our estimation accuracy. Thanks for sharing your expertise!
free-axure-wireframe-kit-1095
Harvey
15.1.2024
Please, could you fix the Kit Download link?. Many Thanks in advance.
Fora Soft Team
15.1.2024
We fixed the link, now the library is available for download! Thanks for your comment
Comments