@ -8,28 +8,32 @@ import android.content.IntentFilter
import android.media.AudioAttributes
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager
import android.media.MediaMetadata
import android.os.Build
import android.os.Build
import android.os.Bundle
import android.os.Bundle
import android.os.IBinder
import android.os.IBinder
import android.os.ResultReceiver
import android.os.ResultReceiver
import android.support.v4.media.session.MediaSession Compat
import android.support.v4.media.MediaMetadata Compat
import android.support.v4.media.session.PlaybackStateCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.view.KeyEvent
import android.view.KeyEvent
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R
import com.github.apognu.otter.R
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import kotlinx.coroutines.CoroutineScope
import com.squareup.picasso.Picasso
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class PlayerService : Service ( ) {
class PlayerService : Service ( ) {
companion object {
const val INITIAL _COMMAND _KEY = " start_command "
}
private var started = false
private var started = false
private val scope : CoroutineScope = CoroutineScope ( Job ( ) + Main )
private val scope : CoroutineScope = CoroutineScope ( Job ( ) + Main )
@ -40,9 +44,10 @@ class PlayerService : Service() {
private lateinit var queue : QueueManager
private lateinit var queue : QueueManager
private lateinit var mediaControlsManager : MediaControlsManager
private lateinit var mediaControlsManager : MediaControlsManager
private lateinit var mediaSession : MediaSessionCompat
private lateinit var player : SimpleExoPlayer
private lateinit var player : SimpleExoPlayer
private val mediaMetadataBuilder = MediaMetadataCompat . Builder ( )
private lateinit var playerEventListener : PlayerEventListener
private lateinit var playerEventListener : PlayerEventListener
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver ( )
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver ( )
@ -51,7 +56,17 @@ class PlayerService : Service() {
private lateinit var radioPlayer : RadioPlayer
private lateinit var radioPlayer : RadioPlayer
override fun onStartCommand ( intent : Intent ? , flags : Int , startId : Int ) : Int {
override fun onStartCommand ( intent : Intent ? , flags : Int , startId : Int ) : Int {
if ( ! started ) watchEventBus ( )
if ( ! started ) {
watchEventBus ( )
intent ?. extras ?. getString ( INITIAL _COMMAND _KEY ) ?. let {
when ( it ) {
Command . ToggleState . toString ( ) -> togglePlayback ( )
Command . NextTrack . toString ( ) -> skipToNextTrack ( )
Command . PreviousTrack . toString ( ) -> skipToPreviousTrack ( )
}
}
}
started = true
started = true
@ -82,18 +97,7 @@ class PlayerService : Service() {
}
}
}
}
mediaSession = MediaSessionCompat ( this , applicationContext . packageName ) . apply {
mediaControlsManager = MediaControlsManager ( this , scope , Otter . get ( ) . mediaSession )
isActive = true
setPlaybackState ( PlaybackStateCompat . Builder ( )
. setActions (
PlaybackStateCompat . ACTION _PLAY _PAUSE or
PlaybackStateCompat . ACTION _SEEK _TO or
PlaybackStateCompat . ACTION _SKIP _TO _NEXT or
PlaybackStateCompat . ACTION _SKIP _TO _PREVIOUS
) . build ( ) )
}
mediaControlsManager = MediaControlsManager ( this , scope , mediaSession )
player = SimpleExoPlayer . Builder ( this ) . build ( ) . apply {
player = SimpleExoPlayer . Builder ( this ) . build ( ) . apply {
playWhenReady = false
playWhenReady = false
@ -102,40 +106,21 @@ class PlayerService : Service() {
addListener ( it )
addListener ( it )
}
}
MediaSessionConnector ( mediaSession ) . also {
MediaSessionConnector ( Otter . get ( ) . mediaSession ) . also {
it . setPlayer ( this )
it . setPlayer ( this )
it . setQueueNavigator ( OtterQueueNavigator ( ) )
it . setMediaMetadataProvider {
it . setMediaMetadataProvider {
mediaControlsManager . buildTrackMetadata ( queue . current ( ) )
buildTrackMetadata ( queue . current ( ) )
}
it . setQueueNavigator ( object : MediaSessionConnector . QueueNavigator {
override fun onSkipToQueueItem ( player : Player , controlDispatcher : ControlDispatcher , id : Long ) { }
override fun onCurrentWindowIndexChanged ( player : Player ) { }
override fun onCommand ( player : Player , controlDispatcher : ControlDispatcher , command : String , extras : Bundle ? , cb : ResultReceiver ? ) = true
override fun getSupportedQueueNavigatorActions ( player : Player ) : Long {
return PlaybackStateCompat . ACTION _PLAY _PAUSE or PlaybackStateCompat . ACTION _SKIP _TO _NEXT or PlaybackStateCompat . ACTION _SKIP _TO _PREVIOUS
}
}
override fun onSkipToNext ( player : Player , controlDispatcher : ControlDispatcher ) { }
override fun getActiveQueueItemId ( player : Player ? ) = 0L
override fun onSkipToPrevious ( player : Player , controlDispatcher : ControlDispatcher ) { }
override fun onTimelineChanged ( player : Player ) { }
} )
it . setMediaButtonEventHandler { player , _ , mediaButtonEvent ->
it . setMediaButtonEventHandler { player , _ , mediaButtonEvent ->
mediaButtonEvent . extras ?. getParcelable < KeyEvent > ( Intent . EXTRA _KEY _EVENT ) ?. let { key ->
mediaButtonEvent . extras ?. getParcelable < KeyEvent > ( Intent . EXTRA _KEY _EVENT ) ?. let { key ->
if ( key . action == KeyEvent . ACTION _UP ) {
if ( key . action == KeyEvent . ACTION _UP ) {
when ( key . keyCode ) {
when ( key . keyCode ) {
KeyEvent . KEYCODE _MEDIA _PLAY -> state ( true )
KeyEvent . KEYCODE _MEDIA _PLAY -> setPlaybackState ( true )
KeyEvent . KEYCODE _MEDIA _PAUSE -> state ( false )
KeyEvent . KEYCODE _MEDIA _PAUSE -> setPlaybackState ( false )
KeyEvent . KEYCODE _MEDIA _NEXT -> player . next ( )
KeyEvent . KEYCODE _MEDIA _NEXT -> player . next ( )
KeyEvent . KEYCODE _MEDIA _PREVIOUS -> previousTrack ( )
KeyEvent . KEYCODE _MEDIA _PREVIOUS -> skipToPreviousTrack ( )
}
}
}
}
}
}
@ -146,12 +131,12 @@ class PlayerService : Service() {
}
}
if ( queue . current > - 1 ) {
if ( queue . current > - 1 ) {
player . prepare ( queue . datasources , true , true )
player . prepare ( queue . datasources )
Cache . get ( this , " progress " ) ?. let { progress ->
Cache . get ( this , " progress " ) ?. let { progress ->
player . seekTo ( queue . current , progress . readLine ( ) . toLong ( ) )
player . seekTo ( queue . current , progress . readLine ( ) . toLong ( ) )
val ( current , duration , percent ) = p rogress( true )
val ( current , duration , percent ) = getP rogress( true )
ProgressBus . send ( current , duration , percent )
ProgressBus . send ( current , duration , percent )
}
}
@ -179,7 +164,7 @@ class PlayerService : Service() {
queue . replace ( command . queue )
queue . replace ( command . queue )
player . prepare ( queue . datasources , true , true )
player . prepare ( queue . datasources , true , true )
state ( true )
setPlaybackS tate ( true )
CommandBus . send ( Command . RefreshTrack ( queue . current ( ) ) )
CommandBus . send ( Command . RefreshTrack ( queue . current ( ) ) )
}
}
@ -193,22 +178,17 @@ class PlayerService : Service() {
queue . current = command . index
queue . current = command . index
player . seekTo ( command . index , C . TIME _UNSET )
player . seekTo ( command . index , C . TIME _UNSET )
state ( true )
setPlaybackS tate ( true )
CommandBus . send ( Command . RefreshTrack ( queue . current ( ) ) )
CommandBus . send ( Command . RefreshTrack ( queue . current ( ) ) )
}
}
is Command . ToggleState -> toggle ( )
is Command . ToggleState -> togglePlayback ( )
is Command . SetState -> state ( command . state )
is Command . SetState -> setPlaybackState ( command . state )
is Command . NextTrack -> {
player . next ( )
Cache . set ( this @PlayerService , " progress " , " 0 " . toByteArray ( ) )
is Command . NextTrack -> skipToNextTrack ( )
ProgressBus . send ( 0 , 0 , 0 )
is Command . PreviousTrack -> skipToPreviousTrack ( )
}
is Command . Seek -> seek ( command . progress )
is Command . PreviousTrack -> previousTrack ( )
is Command . Seek -> progress ( command . progress )
is Command . ClearQueue -> queue . clear ( )
is Command . ClearQueue -> queue . clear ( )
@ -222,10 +202,6 @@ class PlayerService : Service() {
is Command . PinTrack -> PinService . download ( this @PlayerService , command . track )
is Command . PinTrack -> PinService . download ( this @PlayerService , command . track )
is Command . PinTracks -> command . tracks . forEach { PinService . download ( this @PlayerService , it ) }
is Command . PinTracks -> command . tracks . forEach { PinService . download ( this @PlayerService , it ) }
}
}
if ( player . playWhenReady ) {
mediaControlsManager . tick ( )
}
}
}
}
}
@ -243,7 +219,7 @@ class PlayerService : Service() {
while ( true ) {
while ( true ) {
delay ( 1000 )
delay ( 1000 )
val ( current , duration , percent ) = p rogress( )
val ( current , duration , percent ) = getP rogress( )
if ( player . playWhenReady ) {
if ( player . playWhenReady ) {
ProgressBus . send ( current , duration , percent )
ProgressBus . send ( current , duration , percent )
@ -256,6 +232,8 @@ class PlayerService : Service() {
@SuppressLint ( " NewApi " )
@SuppressLint ( " NewApi " )
override fun onDestroy ( ) {
override fun onDestroy ( ) {
scope . cancel ( )
try {
try {
unregisterReceiver ( headphonesUnpluggedReceiver )
unregisterReceiver ( headphonesUnpluggedReceiver )
} catch ( _ : Exception ) {
} catch ( _ : Exception ) {
@ -272,11 +250,8 @@ class PlayerService : Service() {
audioManager . abandonAudioFocus ( audioFocusChangeListener )
audioManager . abandonAudioFocus ( audioFocusChangeListener )
} )
} )
mediaSession . isActive = false
mediaSession . release ( )
player . removeListener ( playerEventListener )
player . removeListener ( playerEventListener )
state ( false )
setPlaybackState ( false )
player . release ( )
player . release ( )
stopForeground ( true )
stopForeground ( true )
@ -286,9 +261,9 @@ class PlayerService : Service() {
}
}
@SuppressLint ( " NewApi " )
@SuppressLint ( " NewApi " )
private fun state ( state : Boolean ) {
private fun setPlaybackS tate ( state : Boolean ) {
if ( ! state ) {
if ( ! state ) {
val ( progress , _ , _ ) = p rogress( )
val ( progress , _ , _ ) = getP rogress( )
Cache . set ( this @PlayerService , " progress " , progress . toString ( ) . toByteArray ( ) )
Cache . set ( this @PlayerService , " progress " , progress . toString ( ) . toByteArray ( ) )
}
}
@ -329,11 +304,11 @@ class PlayerService : Service() {
}
}
}
}
private fun toggle ( ) {
private fun togglePlayback ( ) {
state ( ! player . playWhenReady )
setPlaybackS tate ( ! player . playWhenReady )
}
}
private fun previousTrack ( ) {
private fun ski pToP reviousTrack( ) {
if ( player . currentPosition > 5000 ) {
if ( player . currentPosition > 5000 ) {
return player . seekTo ( 0 )
return player . seekTo ( 0 )
}
}
@ -341,7 +316,14 @@ class PlayerService : Service() {
player . previous ( )
player . previous ( )
}
}
private fun progress ( force : Boolean = false ) : Triple < Int , Int , Int > {
private fun skipToNextTrack ( ) {
player . next ( )
Cache . set ( this @PlayerService , " progress " , " 0 " . toByteArray ( ) )
ProgressBus . send ( 0 , 0 , 0 )
}
private fun getProgress ( force : Boolean = false ) : Triple < Int , Int , Int > {
if ( ! player . playWhenReady && ! force ) return progressCache
if ( ! player . playWhenReady && ! force ) return progressCache
return queue . current ( ) ?. bestUpload ( ) ?. let { upload ->
return queue . current ( ) ?. bestUpload ( ) ?. let { upload ->
@ -354,7 +336,7 @@ class PlayerService : Service() {
} ?: Triple ( 0 , 0 , 0 )
} ?: Triple ( 0 , 0 , 0 )
}
}
private fun progress ( value : Int ) {
private fun seek ( value : Int ) {
val duration = ( ( queue . current ( ) ?. bestUpload ( ) ?. duration ?: 0 ) * ( value . toFloat ( ) / 100 ) ) * 1000
val duration = ( ( queue . current ( ) ?. bestUpload ( ) ?. duration ?: 0 ) * ( value . toFloat ( ) / 100 ) ) * 1000
progressCache = Triple ( duration . toInt ( ) , queue . current ( ) ?. bestUpload ( ) ?. duration ?: 0 , value )
progressCache = Triple ( duration . toInt ( ) , queue . current ( ) ?. bestUpload ( ) ?. duration ?: 0 , value )
@ -362,6 +344,28 @@ class PlayerService : Service() {
player . seekTo ( duration . toLong ( ) )
player . seekTo ( duration . toLong ( ) )
}
}
private fun buildTrackMetadata ( track : Track ? ) : MediaMetadataCompat {
track ?. let {
val coverUrl = maybeNormalizeUrl ( track . album . cover . original )
return mediaMetadataBuilder . apply {
putString ( MediaMetadataCompat . METADATA _KEY _TITLE , track . title )
putString ( MediaMetadataCompat . METADATA _KEY _ARTIST , track . artist . name )
putLong ( MediaMetadata . METADATA _KEY _DURATION , ( track . bestUpload ( ) ?. duration ?. toLong ( ) ?: 0L ) * 1000 )
try {
runBlocking ( IO ) {
this @apply . putBitmap ( MediaMetadataCompat . METADATA _KEY _ALBUM _ART , Picasso . get ( ) . load ( coverUrl ) . get ( ) )
}
} catch ( e : Exception ) {
}
} . build ( )
}
return mediaMetadataBuilder . build ( )
}
@SuppressLint ( " NewApi " )
inner class PlayerEventListener : Player . EventListener {
inner class PlayerEventListener : Player . EventListener {
override fun onPlayerStateChanged ( playWhenReady : Boolean , playbackState : Int ) {
override fun onPlayerStateChanged ( playWhenReady : Boolean , playbackState : Int ) {
super . onPlayerStateChanged ( playWhenReady , playbackState )
super . onPlayerStateChanged ( playWhenReady , playbackState )
@ -388,7 +392,11 @@ class PlayerService : Service() {
if ( playbackState == Player . STATE _READY ) {
if ( playbackState == Player . STATE _READY ) {
mediaControlsManager . updateNotification ( queue . current ( ) , false )
mediaControlsManager . updateNotification ( queue . current ( ) , false )
stopForeground ( false )
Build . VERSION _CODES . N . onApi (
{ stopForeground ( STOP _FOREGROUND _DETACH ) } ,
{ stopForeground ( false ) }
)
}
}
}
}
}
}
@ -441,18 +449,18 @@ class PlayerService : Service() {
AudioManager . AUDIOFOCUS _GAIN -> {
AudioManager . AUDIOFOCUS _GAIN -> {
player . volume = 1f
player . volume = 1f
state ( stateWhenLostFocus )
setPlaybackS tate ( stateWhenLostFocus )
stateWhenLostFocus = false
stateWhenLostFocus = false
}
}
AudioManager . AUDIOFOCUS _LOSS -> {
AudioManager . AUDIOFOCUS _LOSS -> {
stateWhenLostFocus = false
stateWhenLostFocus = false
state ( false )
setPlaybackS tate ( false )
}
}
AudioManager . AUDIOFOCUS _LOSS _TRANSIENT -> {
AudioManager . AUDIOFOCUS _LOSS _TRANSIENT -> {
stateWhenLostFocus = player . playWhenReady
stateWhenLostFocus = player . playWhenReady
state ( false )
setPlaybackS tate ( false )
}
}
AudioManager . AUDIOFOCUS _LOSS _TRANSIENT _CAN _DUCK -> {
AudioManager . AUDIOFOCUS _LOSS _TRANSIENT _CAN _DUCK -> {
@ -462,4 +470,33 @@ class PlayerService : Service() {
}
}
}
}
}
}
inner class OtterQueueNavigator : MediaSessionConnector . QueueNavigator {
override fun onSkipToQueueItem ( player : Player , controlDispatcher : ControlDispatcher , id : Long ) {
CommandBus . send ( Command . PlayTrack ( id . toInt ( ) ) )
}
override fun onCurrentWindowIndexChanged ( player : Player ) { }
override fun onCommand ( player : Player , controlDispatcher : ControlDispatcher , command : String , extras : Bundle ? , cb : ResultReceiver ? ) = true
override fun getSupportedQueueNavigatorActions ( player : Player ) : Long {
return PlaybackStateCompat . ACTION _PLAY _PAUSE or
PlaybackStateCompat . ACTION _SKIP _TO _NEXT or
PlaybackStateCompat . ACTION _SKIP _TO _PREVIOUS or
PlaybackStateCompat . ACTION _SKIP _TO _QUEUE _ITEM
}
override fun onSkipToNext ( player : Player , controlDispatcher : ControlDispatcher ) {
skipToNextTrack ( )
}
override fun getActiveQueueItemId ( player : Player ? ) = queue . current . toLong ( )
override fun onSkipToPrevious ( player : Player , controlDispatcher : ControlDispatcher ) {
skipToPreviousTrack ( )
}
override fun onTimelineChanged ( player : Player ) { }
}
}
}