Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ Interested in seeing a particular feature implemented in this app? Please open a

Contributing
------------
Checkout [CONTRIBUTING.md](https://github.com/SoftwareEngineeringDaily/software-engineering-daily-android/CONTRIBUTING.md) for details.
Checkout [CONTRIBUTING.md](https://github.com/SoftwareEngineeringDaily/software-engineering-daily-android/blob/master/CONTRIBUTING.md) for details.
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class EpisodeDetailFragment : BaseFragment() {
is PlayerStatus.Playing -> showStopViews()
is PlayerStatus.Paused -> showPlayViews()
is PlayerStatus.Ended -> showPlayViews()
is PlayerStatus.Cancelled -> showPlayViews()
is PlayerStatus.Error -> acknowledgeGenericError()
}
}
Expand Down
154 changes: 88 additions & 66 deletions app/src/main/java/com/koalatea/sedaily/feature/player/AudioService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class AudioService : LifecycleService() {

var episodeId: String? = null
private set
private var episodeTitle: String? = null

private lateinit var exoPlayer: SimpleExoPlayer

Expand Down Expand Up @@ -117,75 +118,18 @@ class AudioService : LifecycleService() {
.build()
exoPlayer.setAudioAttributes(audioAttributes, true)

// Monitor ExoPlayer events.
exoPlayer.addListener(PlayerEventListener())
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
handleIntent(intent)

return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
cancelPlaybackMonitor()

mediaSession?.release()
mediaSessionConnector?.setPlayer(null)
playerNotificationManager?.setPlayer(null)

exoPlayer.release()

super.onDestroy()
}

@MainThread
fun play(uri: Uri, startPosition: Long, playbackSpeed: Float? = null) {
val userAgent = Util.getUserAgent(applicationContext, BuildConfig.APPLICATION_ID)
val mediaSource = ExtractorMediaSource(
uri,
DefaultDataSourceFactory(applicationContext, userAgent),
DefaultExtractorsFactory(),
null,
null)

val haveStartPosition = startPosition != C.POSITION_UNSET.toLong()
if (haveStartPosition) {
exoPlayer.seekTo(startPosition)
}

playbackSpeed?.let { changePlaybackSpeed(playbackSpeed) }

exoPlayer.prepare(mediaSource, !haveStartPosition, false)
exoPlayer.playWhenReady = true
}

@MainThread
fun resume() {
exoPlayer.playWhenReady = true
}

@MainThread
fun pause() {
exoPlayer.playWhenReady = false
}

@MainThread
fun changePlaybackSpeed(playbackSpeed: Float) {
exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed)
}

@MainThread
private fun handleIntent(intent: Intent?) {
episodeId = intent?.getStringExtra(ARG_EPISODE_ID)

// Setup notification and media session.
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
applicationContext,
PLAYBACK_CHANNEL_ID,
R.string.playback_channel_name,
PLAYBACK_NOTIFICATION_ID,
object : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): String {
return intent?.getStringExtra(ARG_TITLE) ?: getString(R.string.loading_dots)
return episodeTitle ?: getString(R.string.loading_dots)
}

@Nullable
Expand All @@ -211,13 +155,28 @@ class AudioService : LifecycleService() {
}

override fun onNotificationCancelled(notificationId: Int) {
_playerStatusLiveData.value = PlayerStatus.Cancelled(episodeId)

stopSelf()
}

override fun onNotificationPosted(notificationId: Int, notification: Notification?, ongoing: Boolean) {
if (ongoing) {
// Make sure the service will not get destroyed while playing media.
startForeground(notificationId, notification)
} else {
// Make notification cancellable.
stopForeground(false)
}
}
}
).apply {
// omit skip previous and next actions
// Omit skip previous and next actions.
setUseNavigationActions(false)

// Add stop action.
setUseStopAction(true)

val incrementMs = resources.getInteger(R.integer.increment_ms).toLong()
setFastForwardIncrementMs(incrementMs)
setRewindIncrementMs(incrementMs)
Expand All @@ -240,7 +199,7 @@ class AudioService : LifecycleService() {
putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap)
}

val title = intent?.getStringExtra(ARG_TITLE) ?: getString(R.string.loading_dots)
val title = episodeTitle ?: getString(R.string.loading_dots)

return MediaDescriptionCompat.Builder()
.setIconBitmap(bitmap)
Expand All @@ -252,6 +211,30 @@ class AudioService : LifecycleService() {

setPlayer(exoPlayer)
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
handleIntent(intent)

return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
cancelPlaybackMonitor()

mediaSession?.release()
mediaSessionConnector?.setPlayer(null)
playerNotificationManager?.setPlayer(null)

exoPlayer.release()

super.onDestroy()
}

@MainThread
private fun handleIntent(intent: Intent?) {
episodeId = intent?.getStringExtra(ARG_EPISODE_ID)
episodeTitle = intent?.getStringExtra(ARG_TITLE)

// Play
intent?.let {
Expand All @@ -264,6 +247,42 @@ class AudioService : LifecycleService() {
}
}

@MainThread
fun play(uri: Uri, startPosition: Long, playbackSpeed: Float? = null) {
val userAgent = Util.getUserAgent(applicationContext, BuildConfig.APPLICATION_ID)
val mediaSource = ExtractorMediaSource(
uri,
DefaultDataSourceFactory(applicationContext, userAgent),
DefaultExtractorsFactory(),
null,
null)

val haveStartPosition = startPosition != C.POSITION_UNSET.toLong()
if (haveStartPosition) {
exoPlayer.seekTo(startPosition)
}

playbackSpeed?.let { changePlaybackSpeed(playbackSpeed) }

exoPlayer.prepare(mediaSource, !haveStartPosition, false)
exoPlayer.playWhenReady = true
}

@MainThread
fun resume() {
exoPlayer.playWhenReady = true
}

@MainThread
fun pause() {
exoPlayer.playWhenReady = false
}

@MainThread
fun changePlaybackSpeed(playbackSpeed: Float) {
exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed)
}

@MainThread
private fun saveLastListeningPosition() = lifecycleScope.launch {
episodeId?.let { appDatabase.listenedDao().insert(Listened(it, exoPlayer.contentPosition, exoPlayer.duration)) }
Expand Down Expand Up @@ -321,18 +340,21 @@ class AudioService : LifecycleService() {
if (playbackState == Player.STATE_READY) {
if (exoPlayer.playWhenReady) {
episodeId?.let { _playerStatusLiveData.value = PlayerStatus.Playing(it) }

monitorPlaybackProgress()
} else {// Paused
episodeId?.let { _playerStatusLiveData.value = PlayerStatus.Paused(it) }

cancelPlaybackMonitor()
}
} else if (playbackState == Player.STATE_ENDED) {
episodeId?.let { _playerStatusLiveData.value = PlayerStatus.Ended(it) }
} else {
episodeId?.let { _playerStatusLiveData.value = PlayerStatus.Other(it) }
}

// Only monitor playback to record progress when playing.
if (playbackState == Player.STATE_READY && exoPlayer.playWhenReady) {
monitorPlaybackProgress()
} else {
cancelPlaybackMonitor()
}
}

override fun onPlayerError(e: ExoPlaybackException?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ abstract class BasePlayerActivity : AppCompatActivity(), PlayerCallback, Playbac
_playerStatusLiveData.value = it

playerOverlayPlayMaterialButton.isSelected = it is PlayerStatus.Playing

if (it is PlayerStatus.Cancelled) {
dismissPlayerOverlay()

stopAudioService()
}
})

// Show player after config change.
Expand Down Expand Up @@ -93,11 +99,6 @@ abstract class BasePlayerActivity : AppCompatActivity(), PlayerCallback, Playbac

setupPlayerBottomSheet()

// Show the player, if the audio service is already running.
if (applicationContext.isServiceRunning(AudioService::class.java.name)) {
bindToAudioService()
}

playerOverlayPlayMaterialButton.setOnClickListener {
if (playerOverlayPlayMaterialButton.isSelected) {
audioService?.pause()
Expand Down Expand Up @@ -139,6 +140,17 @@ abstract class BasePlayerActivity : AppCompatActivity(), PlayerCallback, Playbac
playerView.showController()
}

override fun onStart() {
super.onStart()

// Show the player, if the audio service is already running.
if (applicationContext.isServiceRunning(AudioService::class.java.name)) {
bindToAudioService()
} else {
dismissPlayerOverlay()
}
}

override fun onStop() {
unbindAudioService()

Expand All @@ -160,8 +172,12 @@ abstract class BasePlayerActivity : AppCompatActivity(), PlayerCallback, Playbac
override fun stop() {
dismissPlayerOverlay()

audioService?.episodeId?.let { episodeId ->
_playerStatusLiveData.value = PlayerStatus.Paused(episodeId)
} ?: run {
_playerStatusLiveData.value = PlayerStatus.Other()
}
stopAudioService()
_playerStatusLiveData.value = PlayerStatus.Other()
}

private fun bindToAudioService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sealed class PlayerStatus(open val episodeId: String?) {
data class Other(override val episodeId: String? = null) : PlayerStatus(episodeId)
data class Playing(override val episodeId: String) : PlayerStatus(episodeId)
data class Paused(override val episodeId: String) : PlayerStatus(episodeId)
data class Cancelled(override val episodeId: String? = null) : PlayerStatus(episodeId)
data class Ended(override val episodeId: String) : PlayerStatus(episodeId)
data class Error(override val episodeId: String, val exception: Exception?) : PlayerStatus(episodeId)
}