Spin off audio focus into seperate object

Move the code responsible for audio focus into a seperate object to reduce the amount of code in PlaybackService.
This commit is contained in:
OxygenCobalt 2021-01-24 14:35:38 -07:00
parent 3851c59f4b
commit 27d39a1364
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 151 additions and 139 deletions

View file

@ -16,7 +16,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
// SettingsManager is lazy-initted to prevent it from being used before its initialized. // SettingsManager is lazy-initted to prevent it from being used before its initialized.
val settingsManager: SettingsManager by lazy { private val settingsManager: SettingsManager by lazy {
SettingsManager.getInstance() SettingsManager.getInstance()
} }

View file

@ -0,0 +1,107 @@
package org.oxycblt.auxio.playback
import android.animation.ValueAnimator
import android.content.Context
import android.media.AudioManager
import androidx.core.animation.addListener
import androidx.core.content.ContextCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import com.google.android.exoplayer2.SimpleExoPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager
/**
* Object that manages the AudioFocus state.
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
*/
class AudioReactor(
context: Context,
private val player: SimpleExoPlayer
) : AudioManager.OnAudioFocusChangeListener {
private val audioManager = ContextCompat.getSystemService(
context, AudioManager::class.java
) ?: error("Cannot obtain AudioManager.")
private val settingsManager = SettingsManager.getInstance()
private val playbackManager = PlaybackStateManager.getInstance()
private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this)
.build()
private var pauseWasFromAudioFocus = false
/**
* Request the android system for audio focus
*/
fun requestFocus() {
AudioManagerCompat.requestAudioFocus(audioManager, request)
}
/**
* Destroy this object and abandon its audio focus request, should be ran on destruction to
* prevent memory leaks.
*/
fun destroy() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request)
}
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> onGain()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> onDuck()
AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onLoss()
}
}
private fun onGain() {
if (settingsManager.doAudioFocus) {
if (player.volume == VOLUME_DUCK && playbackManager.isPlaying) {
unduck()
} else if (pauseWasFromAudioFocus) {
playbackManager.setPlaying(true)
}
pauseWasFromAudioFocus = false
}
}
private fun onLoss() {
if (settingsManager.doAudioFocus && playbackManager.isPlaying) {
pauseWasFromAudioFocus = true
playbackManager.setPlaying(false)
}
}
private fun onDuck() {
if (settingsManager.doAudioFocus) {
player.volume = VOLUME_DUCK
}
}
private fun unduck() {
player.volume = VOLUME_DUCK
ValueAnimator().apply {
setFloatValues(VOLUME_DUCK, VOLUME_FULL)
duration = DUCK_DURATION
addListener(
onStart = { player.volume = VOLUME_DUCK },
onCancel = { player.volume = VOLUME_FULL },
onEnd = { player.volume = VOLUME_FULL }
)
addUpdateListener {
player.volume = it.animatedValue as Float
}
start()
}
}
companion object {
private const val VOLUME_DUCK = 0.2f
private const val DUCK_DURATION = 1500L
private const val VOLUME_FULL = 1.0f
}
}

View file

@ -1,6 +1,5 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.animation.ValueAnimator
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
@ -16,11 +15,7 @@ import android.os.Parcelable
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.animation.addListener
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
@ -60,8 +55,8 @@ import org.oxycblt.auxio.settings.SettingsManager
* - Audio Focus * - Audio Focus
* - Headset management * - Headset management
* *
* This service relies on [PlaybackStateManager.Callback], so therefore there's no need to bind * This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
* to it to deliver commands. * so therefore there's no need to bind to it to deliver commands.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback, SettingsManager.Callback { class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback, SettingsManager.Callback {
@ -72,7 +67,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private lateinit var systemReceiver: SystemEventReceiver private lateinit var systemReceiver: SystemEventReceiver
private val audioAttributes = AudioAttributes.Builder() private val playerAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MUSIC) .setContentType(C.CONTENT_TYPE_MUSIC)
.build() .build()
@ -80,7 +75,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
private lateinit var notification: NotificationCompat.Builder private lateinit var notification: NotificationCompat.Builder
private lateinit var audioFocusManager: AudioFocusManager private lateinit var audioReactor: AudioReactor
private var isForeground = false private var isForeground = false
private val serviceJob = Job() private val serviceJob = Job()
@ -104,18 +99,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
// --- PLAYER SETUP --- // --- PLAYER SETUP ---
player.addListener(this) player.apply {
addListener(this@PlaybackService)
// Set up AudioFocus/AudioAttributes setAudioAttributes(playerAttributes, false)
player.setAudioAttributes(
audioAttributes, false
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
player.experimentalSetOffloadSchedulingEnabled(true)
} }
audioFocusManager = AudioFocusManager() audioReactor = AudioReactor(this, player)
// --- SYSTEM RECEIVER SETUP --- // --- SYSTEM RECEIVER SETUP ---
@ -153,13 +142,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
// --- NOTIFICATION SETUP --- // --- NOTIFICATION SETUP ---
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notification = notificationManager.createMediaNotification(this, mediaSession) notification = notificationManager.createMediaNotification(this, mediaSession)
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.resetHasPlayedStatus() playbackManager.resetHasPlayedStatus()
playbackManager.addCallback(this) playbackManager.addCallback(this)
if (playbackManager.song != null || playbackManager.isRestored) { if (playbackManager.song != null || playbackManager.isRestored) {
@ -181,7 +168,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
// Release everything that could cause a memory leak if left around // Release everything that could cause a memory leak if left around
player.release() player.release()
mediaSession.release() mediaSession.release()
audioFocusManager.destroy() audioReactor.destroy()
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
settingsManager.removeCallback(this) settingsManager.removeCallback(this)
@ -261,7 +248,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
if (isPlaying && !player.isPlaying) { if (isPlaying && !player.isPlaying) {
player.play() player.play()
notification.updatePlaying(this) notification.updatePlaying(this)
audioFocusManager.requestFocus() audioReactor.requestFocus()
startForegroundOrNotify() startForegroundOrNotify()
startPollingPosition() startPollingPosition()
@ -298,12 +285,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
player.seekTo(position) player.seekTo(position)
} }
override fun onRestoreFinish() {
logD("Restore done")
restorePlayer()
}
// --- SETTINGSMANAGER OVERRIDES --- // --- SETTINGSMANAGER OVERRIDES ---
override fun onColorizeNotifUpdate(doColorize: Boolean) { override fun onColorizeNotifUpdate(doColorize: Boolean) {
@ -518,86 +499,10 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
return false return false
} }
/**
* Object that manages the AudioFocus state.
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
*/
inner class AudioFocusManager : AudioManager.OnAudioFocusChangeListener {
private val audioManager = ContextCompat.getSystemService(
this@PlaybackService, AudioManager::class.java
) ?: error("Cannot obtain AudioManager.")
private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this)
.build()
private var pauseWasFromAudioFocus = false
fun requestFocus() {
AudioManagerCompat.requestAudioFocus(audioManager, request)
}
fun destroy() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request)
}
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> onGain()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> onDuck()
AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onLoss()
}
}
private fun onGain() {
if (settingsManager.doAudioFocus) {
if (player.volume == VOLUME_DUCK && playbackManager.isPlaying) {
unduck()
} else if (pauseWasFromAudioFocus) {
playbackManager.setPlaying(true)
}
pauseWasFromAudioFocus = false
}
}
private fun onLoss() {
if (settingsManager.doAudioFocus && playbackManager.isPlaying) {
pauseWasFromAudioFocus = true
playbackManager.setPlaying(false)
}
}
private fun onDuck() {
if (settingsManager.doAudioFocus) {
player.volume = VOLUME_DUCK
}
}
private fun unduck() {
player.volume = VOLUME_DUCK
ValueAnimator().apply {
setFloatValues(VOLUME_DUCK, VOLUME_FULL)
duration = DUCK_DURATION
addListener(
onStart = { player.volume = VOLUME_DUCK },
onCancel = { player.volume = VOLUME_FULL },
onEnd = { player.volume = VOLUME_FULL }
)
addUpdateListener {
player.volume = it.animatedValue as Float
}
start()
}
}
}
/** /**
* A [BroadcastReceiver] for receiving system events from the media notification or the headset. * A [BroadcastReceiver] for receiving system events from the media notification or the headset.
*/ */
private inner class SystemEventReceiver : BroadcastReceiver() { inner class SystemEventReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val action = intent.action val action = intent.action
@ -605,12 +510,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
when (it) { when (it) {
NotificationUtils.ACTION_LOOP -> NotificationUtils.ACTION_LOOP ->
playbackManager.setLoopMode(playbackManager.loopMode.increment()) playbackManager.setLoopMode(playbackManager.loopMode.increment())
NotificationUtils.ACTION_SHUFFLE -> NotificationUtils.ACTION_SHUFFLE ->
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true) playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev() NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev()
NotificationUtils.ACTION_PLAY_PAUSE -> {
NotificationUtils.ACTION_PLAY_PAUSE ->
playbackManager.setPlaying(!playbackManager.isPlaying) playbackManager.setPlaying(!playbackManager.isPlaying)
}
NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next() NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next()
NotificationUtils.ACTION_EXIT -> stop() NotificationUtils.ACTION_EXIT -> stop()
@ -670,9 +578,5 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
companion object { companion object {
private const val DISCONNECTED = 0 private const val DISCONNECTED = 0
private const val CONNECTED = 1 private const val CONNECTED = 1
private const val VOLUME_DUCK = 0.2f
private const val DUCK_DURATION = 1500L
private const val VOLUME_FULL = 1.0f
} }
} }

View file

@ -246,32 +246,6 @@ class PlaybackStateManager private constructor() {
} }
} }
/**
* Update the current position. Will not notify any listeners of a seek event, that's what [seekTo] is for.
* @param position The new position in millis.
* @see seekTo
*/
fun setPosition(position: Long) {
mSong?.let {
// Don't accept any bugged positions that are over the duration of the song.
if (position <= it.duration) {
mPosition = position
}
}
}
/**
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* @param position The position to seek to in millis.
* @see setPosition
*/
fun seekTo(position: Long) {
mPosition = position
callbacks.forEach { it.onSeek(position) }
}
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
/** /**
@ -574,6 +548,32 @@ class PlaybackStateManager private constructor() {
} }
} }
/**
* Update the current position. Will not notify any listeners of a seek event, that's what [seekTo] is for.
* @param position The new position in millis.
* @see seekTo
*/
fun setPosition(position: Long) {
mSong?.let {
// Don't accept any bugged positions that are over the duration of the song.
if (position <= it.duration) {
mPosition = position
}
}
}
/**
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* @param position The position to seek to in millis.
* @see setPosition
*/
fun seekTo(position: Long) {
mPosition = position
callbacks.forEach { it.onSeek(position) }
}
/** /**
* Rewind to the beginning of a song. * Rewind to the beginning of a song.
*/ */

View file

@ -16,6 +16,7 @@ import kotlin.reflect.KProperty
* A delegate that creates a binding that can be used as a member variable without nullability or * A delegate that creates a binding that can be used as a member variable without nullability or
* memory leaks. * memory leaks.
* @param inflate The ViewBinding inflation method that should be used * @param inflate The ViewBinding inflation method that should be used
* @param onDestroy What to do when the binding is destroyed
*/ */
fun <T : ViewDataBinding> Fragment.memberBinding( fun <T : ViewDataBinding> Fragment.memberBinding(
inflate: (LayoutInflater) -> T, inflate: (LayoutInflater) -> T,