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

@ -10,9 +10,6 @@ package io.element.android.features.call.impl.ui
import android.Manifest
import android.app.PictureInPictureParams
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.util.Rational
@ -46,6 +43,8 @@ import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
@ -65,16 +64,12 @@ class ElementCallActivity :
@Inject lateinit var enterpriseService: EnterpriseService
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var audioFocus: AudioFocus
private lateinit var presenter: Presenter<CallScreenState>
private lateinit var audioManager: AudioManager
private var requestPermissionCallback: RequestPermissionCallback? = null
private var audiofocusRequest: AudioFocusRequest? = null
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
private val requestPermissionsLauncher = registerPermissionResultLauncher()
private val webViewTarget = mutableStateOf<CallType?>(null)
@ -102,8 +97,6 @@ class ElementCallActivity :
pictureInPicturePresenter.setPipView(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
setContent {
val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
@ -133,7 +126,13 @@ class ElementCallActivity :
}
private fun setCallIsActive() {
requestAudioFocus()
audioFocus.requestAudioFocus(
requester = AudioFocusRequester.ElementCall,
onFocusLost = {
// If the audio focus is lost, we do not stop the call.
Timber.tag(loggerTag.value).w("Audio focus lost")
}
)
CallForegroundService.start(this)
}
@ -175,7 +174,7 @@ class ElementCallActivity :
override fun onDestroy() {
super.onDestroy()
releaseAudioFocus()
audioFocus.releaseAudioFocus()
CallForegroundService.stop(this)
pictureInPicturePresenter.setPipView(null)
}
@ -241,37 +240,6 @@ class ElementCallActivity :
}
}
@Suppress("DEPRECATION")
private fun requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build()
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.build()
audioManager.requestAudioFocus(request)
audiofocusRequest = request
} else {
val listener = AudioManager.OnAudioFocusChangeListener { }
audioManager.requestAudioFocus(
listener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
)
audioFocusChangeListener = listener
}
}
@Suppress("DEPRECATION")
private fun releaseAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audiofocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
} else {
audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) }
}
}
@RequiresApi(Build.VERSION_CODES.O)
override fun setPipParams() {
setPictureInPictureParams(getPictureInPictureParams())