Skip to content

Commit

Permalink
fix(android): Avoid playing after gaining focus in paused state (#1857)
Browse files Browse the repository at this point in the history
# Description

Avoid playing when the app gains audio focus, e.g. after a phone call.
Also pause the audio when losing focus, or only pause it temporarly if
losing transient focus.

## Related Issues

closes #1853
closes #1854
closes #1688
  • Loading branch information
Gustl22 authored Nov 18, 2024
1 parent 78a5683 commit 01726c1
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ class FocusManager(
private val audioManager: AudioManager
get() = player.audioManager

fun maybeRequestAudioFocus(andThen: () -> Unit) {
fun maybeRequestAudioFocus(onGranted: () -> Unit, onLoss: (isTransient: Boolean) -> Unit) {
if (context.audioFocus == AudioManager.AUDIOFOCUS_NONE) {
andThen()
onGranted()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
newRequestAudioFocus(andThen)
newRequestAudioFocus(onGranted, onLoss)
} else {
@Suppress("DEPRECATION")
oldRequestAudioFocus(andThen)
oldRequestAudioFocus(onGranted, onLoss)
}
}

Expand All @@ -41,35 +41,47 @@ class FocusManager(
}

@RequiresApi(Build.VERSION_CODES.O)
private fun newRequestAudioFocus(andThen: () -> Unit) {
private fun newRequestAudioFocus(onGranted: () -> Unit, onLoss: (isTransient: Boolean) -> Unit) {
val audioFocus = context.audioFocus

// Listen also for focus changes, e.g. if interrupt playing with a phone call and resume afterward.
val audioFocusRequest = AudioFocusRequest.Builder(audioFocus)
.setAudioAttributes(context.buildAttributes())
.setOnAudioFocusChangeListener { handleFocusResult(it, andThen) }
.setOnAudioFocusChangeListener { handleFocusResult(it, onGranted, onLoss) }
.build()
this.audioFocusRequest = audioFocusRequest

val result = audioManager.requestAudioFocus(audioFocusRequest)
handleFocusResult(result, andThen)
handleFocusResult(result, onGranted, onLoss)
}

@Deprecated("Use requestAudioFocus instead")
private fun oldRequestAudioFocus(andThen: () -> Unit) {
private fun oldRequestAudioFocus(onGranted: () -> Unit, onLoss: (isTransient: Boolean) -> Unit) {
val audioFocus = context.audioFocus
audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { handleFocusResult(it, andThen) }
audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { handleFocusResult(it, onGranted, onLoss) }
@Suppress("DEPRECATION")
val result = audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_MUSIC,
audioFocus,
)
handleFocusResult(result, andThen)
handleFocusResult(result, onGranted, onLoss)
}

private fun handleFocusResult(result: Int, andThen: () -> Unit) {
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
andThen()
private fun handleFocusResult(result: Int, onGranted: () -> Unit, onLoss: (isTransient: Boolean) -> Unit) {
when (result) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
onGranted()
}

AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
onLoss(true)
}

AudioManager.AUDIOFOCUS_LOSS -> {
onLoss(false)
}
}
// Keep playing source on `AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK` as sound is ducked.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,37 @@ class WrappedPlayer internal constructor(
* Playback handling methods
*/
fun play() {
focusManager.maybeRequestAudioFocus(andThen = ::actuallyPlay)
}

private fun actuallyPlay() {
if (!playing && !released) {
val currentPlayer = player
playing = true
if (currentPlayer == null) {
if (player == null) {
initPlayer()
} else if (prepared) {
currentPlayer.start()
requestFocusAndStart()
}
}
}

// Try to get audio focus and then start.
private fun requestFocusAndStart() {
focusManager.maybeRequestAudioFocus(
onGranted = {
// Check if in playing state, as the focus can also be gained e.g. after a phone call, even if not playing.
if (playing) {
player?.start()
}
},
onLoss = { isTransient ->
if (isTransient) {
// Do not check or set playing state, as the state should be recovered after granting focus again.
player?.pause()
} else {
// Audio focus won't be recovered
pause()
}
},
)
}

fun stop() {
focusManager.handleStop()
if (released) {
Expand Down Expand Up @@ -271,7 +287,7 @@ class WrappedPlayer internal constructor(
prepared = true
ref.handleDuration(this)
if (playing) {
player?.start()
requestFocusAndStart()
}
if (shouldSeekTo >= 0 && player?.isLiveStream() != true) {
player?.seekTo(shouldSeekTo)
Expand Down

0 comments on commit 01726c1

Please sign in to comment.