Improve audio focus management (#4707)

* Extract Audio focus management to its own modules.

* Request Audio focus when playing a voice message.

* Add missing dependency.
(and remove a duplicated one)

* Request Audio focus when playing a video/audio in the media viewer.

* Pause audio when audio focus is lost.

* Rename class

* Fix tests

* Fix detekt issue.

* Audio focus: let the system handle automatic ducking when playing media.

* Document and update API

* Remove useless space.
This commit is contained in:
Benoit Marty 2025-05-13 11:54:58 +02:00 committed by GitHub
parent 3cf8b04a07
commit 93499910ba
24 changed files with 341 additions and 49 deletions

View file

@ -0,0 +1,25 @@
import extension.setupAnvil
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.audio.impl"
}
setupAnvil()
dependencies {
api(projects.libraries.audio.api)
implementation(libs.androidx.corektx)
implementation(libs.dagger)
implementation(projects.libraries.di)
}

View file

@ -0,0 +1,106 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.audio.impl
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import androidx.core.content.getSystemService
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultAudioFocus @Inject constructor(
@ApplicationContext private val context: Context,
) : AudioFocus {
private val audioManager = requireNotNull(context.getSystemService<AudioManager>())
private var audioFocusRequest: AudioFocusRequest? = null
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
@Suppress("DEPRECATION")
override fun requestAudioFocus(
requester: AudioFocusRequester,
onFocusLost: () -> Unit,
) {
val listener = AudioManager.OnAudioFocusChangeListener {
when (it) {
AudioManager.AUDIOFOCUS_GAIN -> {
// Do nothing
}
AudioManager.AUDIOFOCUS_LOSS,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
onFocusLost()
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(requester.toAudioUsage())
.build()
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(listener)
.setWillPauseWhenDucked(requester.willPausedWhenDucked())
.build()
audioManager.requestAudioFocus(request)
audioFocusRequest = request
} else {
audioManager.requestAudioFocus(
listener,
requester.toAudioStream(),
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
)
audioFocusChangeListener = listener
}
}
@Suppress("DEPRECATION")
override fun releaseAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
} else {
audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) }
}
}
}
private fun AudioFocusRequester.toAudioUsage(): Int {
return when (this) {
AudioFocusRequester.ElementCall,
AudioFocusRequester.VoiceMessage -> AudioAttributes.USAGE_VOICE_COMMUNICATION
AudioFocusRequester.MediaViewer -> AudioAttributes.USAGE_MEDIA
}
}
private fun AudioFocusRequester.toAudioStream(): Int {
return when (this) {
AudioFocusRequester.ElementCall,
AudioFocusRequester.VoiceMessage -> AudioManager.STREAM_VOICE_CALL
AudioFocusRequester.MediaViewer -> AudioManager.STREAM_MUSIC
}
}
private fun AudioFocusRequester.willPausedWhenDucked(): Boolean {
return when (this) {
// (note that for Element Call, there is no action when the focus is lost)
AudioFocusRequester.ElementCall,
AudioFocusRequester.VoiceMessage -> true
// For the MediaViewer, we let the system automatically handle the ducking
// https://developer.android.com/media/optimize/audio-focus#automatic-ducking
AudioFocusRequester.MediaViewer -> false
}
}