123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- package fr.forum_thalie.tsumugi
-
- import android.app.Service
- import android.content.BroadcastReceiver
- import android.content.Context
- import android.content.Intent
- import android.content.IntentFilter
- import android.graphics.Bitmap
- import android.support.v4.media.MediaBrowserCompat
- import android.util.Log
- import androidx.media.MediaBrowserServiceCompat
- import android.media.AudioManager
- import android.os.*
- import android.support.v4.media.session.MediaSessionCompat
- import android.support.v4.media.session.PlaybackStateCompat
- import androidx.media.session.MediaButtonReceiver
- import com.google.android.exoplayer2.source.ProgressiveMediaSource
- import com.google.android.exoplayer2.util.Util.getUserAgent
- import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
-
- import android.net.Uri
- import android.support.v4.media.MediaMetadataCompat
- import android.telephony.PhoneStateListener
- import android.telephony.TelephonyManager
- import android.view.KeyEvent
- import androidx.core.app.NotificationCompat
- import androidx.core.content.edit
- import androidx.lifecycle.Observer
- import androidx.media.AudioAttributesCompat
- import androidx.media.AudioFocusRequestCompat
- import androidx.media.AudioManagerCompat
- import androidx.preference.PreferenceManager
- import com.google.android.exoplayer2.*
- import com.google.android.exoplayer2.metadata.icy.*
- import fr.forum_thalie.tsumugi.alarm.RadioAlarm
- import fr.forum_thalie.tsumugi.alarm.RadioSleeper
- import fr.forum_thalie.tsumugi.planning.Planning
- import fr.forum_thalie.tsumugi.playerstore.PlayerStore
- import java.util.*
- import kotlin.math.exp
- import kotlin.math.ln
- import kotlin.system.exitProcess
-
-
- class RadioService : MediaBrowserServiceCompat() {
-
- private val radioTag = "======RadioService====="
- private lateinit var nowPlayingNotification: NowPlayingNotification
- private val radioServiceId = 1
- private var numberOfSongs = 0
- private val apiTicker: Timer = Timer()
- private var isAlarmStopped: Boolean = false
-
- // Define the broadcast receiver to handle any broadcasts
- private val receiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val action = intent.action
- if (action != null && action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
- val i = Intent(context, RadioService::class.java)
- i.putExtra("action", Actions.STOP.name)
- context.startService(i)
- }
- if (action != null && action == Intent.ACTION_HEADSET_PLUG)
- {
- var headsetPluggedIn = false
-
- // In the Intent state there's the value whether the headphones are plugged or not.
- // This *should* work in any case...
- when (intent.getIntExtra("state", -1)) {
- 0 -> {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
- }
- 1 -> {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
- headsetPluggedIn = true && PreferenceManager.getDefaultSharedPreferences(context).getBoolean("autoStartOnPlug", false)
- }
- else -> {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "I have no idea what the headset state is")
- }
- }
- /*
- val am = getSystemService(AUDIO_SERVICE) as AudioManager
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
- {
- val devices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
- for (d in devices)
- {
- if (d.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || d.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
- headsetPluggedIn = true
- }
- }
- else
- {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Can't get state?")
- }
-
- */
- if((mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_STOPPED
- || mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_PAUSED)
- && headsetPluggedIn)
- beginPlaying()
- }
- }
- }
-
- // ##################################################
- // ################### OBSERVERS ####################
- // ##################################################
-
- private val volumeObserver: Observer<Int> = Observer {
- setVolume(it)
- }
-
- private val isMutedObserver: Observer<Boolean> = Observer {
- setVolume(if (it) null else -1)
- }
-
- private val isPlayingObserver: Observer<Boolean> = Observer {
- if (it)
- beginPlaying()
- else
- stopPlaying()
- }
-
- private val titleObserver = Observer<String> {
- // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
-
- if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
- {
- //[REMOVE LOG CALLS]Log.d((tag, radioTag + "SONG CHANGED AND PLAYING")
- // we activate latency compensation only if it's been at least 2 songs...
- when {
- PlayerStore.instance.isStreamDown -> {
- // if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
- // so the stream is now back to normal.
- PlayerStore.instance.isStreamDown = false
- PlayerStore.instance.initApi()
- }
- PlayerStore.instance.currentSong.title.value == noConnectionValue -> {
- PlayerStore.instance.isStreamDown = true
- }
- else -> {
- PlayerStore.instance.fetchApi(/* numberOfSongs >= 2 */)
- }
- }
- }
-
- if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
- && it != noConnectionValue)
- {
- PlayerStore.instance.updateLp()
- PlayerStore.instance.updateQueue()
- }
- nowPlayingNotification.update(this)
- Planning.instance.checkProgramme()
- }
-
- private val streamerPictureObserver = Observer<Bitmap> {
- nowPlayingNotification.update(this)
- }
-
- // ##################################################
- // ############## LIFECYCLE CALLBACKS ###############
- // ##################################################
-
- override fun onLoadChildren(
- parentId: String,
- result: Result<MutableList<MediaBrowserCompat.MediaItem>>
- ) {
- result.sendResult(null)
- }
-
- override fun onGetRoot(
- clientPackageName: String,
- clientUid: Int,
- rootHints: Bundle?
- ): BrowserRoot? {
- // Clients can connect, but you can't browse internet radio
- // so onLoadChildren returns nothing. This disables the ability to browse for content.
- return BrowserRoot(getString(R.string.MEDIA_ROOT_ID), null)
- }
-
- override fun onCreate() {
- super.onCreate()
-
- preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
-
- // start ticker for when the player is stopped
- val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
- val period: Long = Integer.parseInt(periodString).toLong()
- if (period > 0)
- apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
-
- // Define managers
- telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
- telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
- audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
-
- //define the audioFocusRequest
- val audioFocusRequestBuilder = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
- audioFocusRequestBuilder.setOnAudioFocusChangeListener(focusChangeListener)
- val audioAttributes = AudioAttributesCompat.Builder()
- audioAttributes.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
- audioAttributes.setUsage(AudioAttributesCompat.USAGE_MEDIA)
- audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
- audioFocusRequest = audioFocusRequestBuilder.build()
-
- // This stuff is for the broadcast receiver
- val filter = IntentFilter()
- filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
- filter.addAction(Intent.ACTION_HEADSET_PLUG)
- registerReceiver(receiver, filter)
-
- // setup media player
- setupMediaPlayer()
- createMediaSession()
-
- nowPlayingNotification = NowPlayingNotification(
- notificationChannelId = this.getString(R.string.nowPlayingChannelId),
- notificationChannel = R.string.nowPlayingNotificationChannel,
- notificationId = 1,
- notificationImportance = NotificationCompat.PRIORITY_LOW
- )
- nowPlayingNotification.create(this, mediaSession)
-
-
- PlayerStore.instance.currentSong.title.observeForever(titleObserver)
- PlayerStore.instance.volume.observeForever(volumeObserver)
- PlayerStore.instance.isPlaying.observeForever(isPlayingObserver)
- PlayerStore.instance.isMuted.observeForever(isMutedObserver)
- PlayerStore.instance.streamerPicture.observeForever(streamerPictureObserver)
-
- startForeground(radioServiceId, nowPlayingNotification.notification)
-
- PlayerStore.instance.isServiceStarted.value = true
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "created")
- }
-
- private val handler = Handler()
- class LowerVolumeRunnable : Runnable {
- override fun run() {
- PlayerStore.instance.volume.postValue(
- (PlayerStore.instance.volume.value!!.toFloat() * (9f / 10f)).toInt()
- ) // the setVolume is called by the volumeObserver in RadioService (on main thread for ExoPlayer!)
- }
- }
- private val lowerVolumeRunnable = LowerVolumeRunnable()
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (intent?.getStringExtra("action") == null)
- return super.onStartCommand(intent, flags, startId)
-
- if (MediaButtonReceiver.handleIntent(mediaSession, intent) != null)
- return super.onStartCommand(intent, flags, startId)
-
- when (intent.getStringExtra("action")) {
- Actions.PLAY.name -> beginPlaying()
- Actions.STOP.name -> { setVolume(PlayerStore.instance.volume.value); stopPlaying() } // setVolume is here to reset the volume to the user's preference when the alarm (that sets volume to 100) is dismissed
- Actions.PAUSE.name -> { setVolume(PlayerStore.instance.volume.value); pausePlaying() }
- Actions.VOLUME.name -> setVolume(intent.getIntExtra("value", 100))
- Actions.KILL.name -> {stopForeground(true); stopSelf(); return Service.START_NOT_STICKY}
- Actions.NOTIFY.name -> nowPlayingNotification.update(this)
- Actions.PLAY_OR_FALLBACK.name -> beginPlayingOrFallback()
- Actions.FADE_OUT.name -> {
- for (i in 1 until 28) // we schedule 28 "LowerVolumeRunnable" every 2 seconds (i * 2)
- {
- // I couldn't find how to send multiple times the same PendingIntent using AlarmManager, so I relied on Handler instead.
- // I think there's no guarantee of exact time with the Handler, especially when the device is in deep sleep,
- // But when I force-set the Deep Sleep mode with ADB, it worked fine, so I'll leave it as this.
- // BUT! SOMETIMES IT DIDN'T WORK AND I DON'T KNOW WHY.
- // I hope that moving the handler in the RadioService would solve the issue and trigger it correctly.
- handler.postDelayed(lowerVolumeRunnable, (i * 2 * 1000).toLong())
- }
- }
- Actions.CANCEL_FADE_OUT.name -> { handler.removeCallbacks(lowerVolumeRunnable) }
- Actions.SNOOZE.name -> { RadioAlarm.instance.snooze(this) }
- }
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
- super.onStartCommand(intent, flags, startId)
- // The service must be re-created if it is destroyed by the system. This allows the user to keep actions like Bluetooth and headphones plug available.
- return START_STICKY
- }
-
- override fun onTaskRemoved(rootIntent: Intent) {
- if (mediaSession.controller.playbackState.state != PlaybackStateCompat.STATE_PLAYING) {
- nowPlayingNotification.clear()
- stopSelf()
- }
- super.onTaskRemoved(rootIntent)
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "task removed")
- }
-
- override fun onDestroy() {
- super.onDestroy()
- player.stop()
- player.release()
- unregisterReceiver(receiver)
- PlayerStore.instance.currentSong.title.removeObserver(titleObserver)
- PlayerStore.instance.volume.removeObserver(volumeObserver)
- PlayerStore.instance.isPlaying.removeObserver(isPlayingObserver)
- PlayerStore.instance.isMuted.removeObserver(isMutedObserver)
- PlayerStore.instance.streamerPicture.removeObserver(streamerPictureObserver)
-
-
- mediaSession.isActive = false
- mediaSession.setMediaButtonReceiver(null)
-
- mediaSession.release()
-
- PlayerStore.instance.isServiceStarted.value = false
- PlayerStore.instance.isInitialized = false
-
-
- RadioSleeper.instance.cancelSleep(this, isClosing = true)
-
- PreferenceManager.getDefaultSharedPreferences(this).edit {
- this.putBoolean("isSleeping", false)
- this.commit()
- }
-
- apiTicker.cancel() // stops the timer.
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "destroyed")
- // if the service is destroyed, the application had become useless.
- exitProcess(0)
- }
-
- // ########################################
- // ######## AUDIO FOCUS MANAGEMENT ########
- //#########################################
-
- // Define the managers
- private var telephonyManager: TelephonyManager? = null
- private lateinit var audioManager: AudioManager
- private lateinit var audioFocusRequest: AudioFocusRequestCompat
-
- private val phoneStateListener = object : PhoneStateListener() {
- override fun onCallStateChanged(state: Int, incomingNumber: String) {
- super.onCallStateChanged(state, incomingNumber)
-
- if (state != TelephonyManager.CALL_STATE_IDLE) {
- setVolume(0)
- } else {
- setVolume(PlayerStore.instance.volume.value!!)
- }
- }
- }
-
- // Define the listener that will control what happens when focus is changed
- private val focusChangeListener =
- AudioManager.OnAudioFocusChangeListener { focusChange ->
- when (focusChange) {
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> setVolume((0.20f * PlayerStore.instance.volume.value!!).toInt()) //20% of current volume.
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> setVolume(0)
- AudioManager.AUDIOFOCUS_LOSS -> stopPlaying()
- AudioManager.AUDIOFOCUS_GAIN -> setVolume(PlayerStore.instance.volume.value!!)
- else -> {}
- }
- }
-
- // ########################################
- // ######## MEDIA PLAYER / SESSION ########
- // ########################################
-
- private lateinit var mediaSession : MediaSessionCompat
- private lateinit var playbackStateBuilder: PlaybackStateCompat.Builder
- private lateinit var metadataBuilder: MediaMetadataCompat.Builder
- private lateinit var player: SimpleExoPlayer
- private lateinit var radioMediaSource: ProgressiveMediaSource
- private lateinit var fallbackMediaSource: ProgressiveMediaSource
-
- private fun setupMediaPlayer(){
-
- val minBufferMillis = 15 * 1000 // Default value
- val maxBufferMillis = 50 * 1000 // Default value
- val bufferForPlayback = 4 * 1000 // Default is 2.5s.
- // Increasing it makes it more robust to short connection loss, at the expense of latency when we press Play. 4s seems reasonable to me.
- val bufferForPlaybackAfterRebuffer = 7 * 1000 // Default is 5s.
-
- val loadControl = DefaultLoadControl.Builder().apply {
- setBufferDurationsMs(minBufferMillis, maxBufferMillis, bufferForPlayback, bufferForPlaybackAfterRebuffer)
- }.createDefaultLoadControl()
-
-
- val playerBuilder = SimpleExoPlayer.Builder(this)
- playerBuilder.setLoadControl(loadControl)
- player = playerBuilder.build()
-
-
- player.addMetadataOutput {
- for (i in 0 until it.length()) {
- val entry = it.get(i)
- if (entry is IcyHeaders) {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
- }
- if (entry is IcyInfo) {
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
- // Note : Kotlin supports UTF-8 by default.
- numberOfSongs++
- val data = entry.title!!
- PlayerStore.instance.currentSong.setTitleArtist(data)
- }
- val d : Long = ((PlayerStore.instance.currentSong.stopTime.value?.minus(PlayerStore.instance.currentSong.startTime.value!!) ?: 0) / 1000)
- val duration = if (d > 0) d - (PlayerStore.instance.latencyCompensator) else 0
- metadataBuilder.putString(
- MediaMetadataCompat.METADATA_KEY_TITLE,
- PlayerStore.instance.currentSong.title.value
- )
- .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, PlayerStore.instance.currentSong.artist.value)
- .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
-
- mediaSession.setMetadata(metadataBuilder.build())
-
- val intent = Intent("com.android.music.metachanged")
- intent.putExtra("artist", PlayerStore.instance.currentSong.artist.value)
- intent.putExtra("track", PlayerStore.instance.currentSong.title.value)
- intent.putExtra("duration", duration)
- intent.putExtra("position", 0)
- sendBroadcast(intent)
- }
- }
- // this listener allows to reset numberOfSongs if the connection is lost.
- player.addListener(exoPlayerEventListener)
-
- // Produces DataSource instances through which media data is loaded.
- val dataSourceFactory = DefaultDataSourceFactory(
- this,
- getUserAgent(this, getString(R.string.app_name))
- )
- // This is the MediaSource representing the media to be played.
- radioMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
- .createMediaSource(Uri.parse(getString(R.string.STREAM_URL_TSUMUGI)))
-
- fallbackMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
- .createMediaSource(Uri.parse("file:///android_asset/the_stream_is_down.mp3"))
- }
-
- private fun createMediaSession() {
- mediaSession = MediaSessionCompat(this, "RadioMediaSession")
- // Deprecated flags
- // mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS and MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
- mediaSession.isActive = true
- mediaSession.setCallback(mediaSessionCallback)
- playbackStateBuilder = PlaybackStateCompat.Builder()
- playbackStateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
- .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f, SystemClock.elapsedRealtime())
-
- metadataBuilder = MediaMetadataCompat.Builder()
- mediaSession.setPlaybackState(playbackStateBuilder.build())
- }
-
- // ########################################
- // ######### SERVICE START/STOP ###########
- // ########################################
-
- // this function is playing the stream if available, or a default sound if there's a problem.
- private fun beginPlayingOrFallback()
- {
- PlayerStore.instance.volume.value = PreferenceManager.getDefaultSharedPreferences(this).getInt("alarmVolume", 100)
- // we set the max volume for exoPlayer to be sure it rings correctly.
- beginPlaying(isRinging = true, isFallback = false)
- val wait: (Any?) -> Any = {
- /*
- Here we lower the isAlarmStopped flag and we wait for 17s. (seems like 12 could be a bit too short since I increased the buffer!!)
- If the user stops the alarm (by calling an intent), the isAlarmStopped flag will be raised.
- */
- isAlarmStopped = false // reset the flag
- var i = 0
- while (i < 17)
- {
- Thread.sleep(1000)
- i++
- //[REMOVE LOG CALLS]Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
- }
- }
- val post: (Any?) -> Unit = {
- // we verify : if the player is not playing, and if the user didn't stop it, it means that there's a network issue.
- // So we use the fallback sound to wake up the user!!
- // (note: player.isPlaying is only accessible on main thread, so we can't check in the wait() lambda)
- if (!player.isPlaying && !isAlarmStopped)
- beginPlaying(isRinging = true, isFallback = true)
- }
- Async(wait, post)
- }
-
- fun beginPlaying(isRinging: Boolean = false, isFallback: Boolean = false)
- {
- //define the audioFocusRequest
- val audioFocusRequestBuilder = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
- audioFocusRequestBuilder.setOnAudioFocusChangeListener(focusChangeListener)
- val audioAttributes = AudioAttributesCompat.Builder()
- audioAttributes.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
- if (isRinging)
- {
- audioAttributes.setUsage(AudioAttributesCompat.USAGE_ALARM)
- audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
- audioFocusRequest = audioFocusRequestBuilder.build()
- player.audioAttributes = com.google.android.exoplayer2.audio.AudioAttributes
- .Builder()
- .setContentType(C.CONTENT_TYPE_MUSIC)
- .setUsage(C.USAGE_ALARM)
- .build()
- } else {
- isAlarmStopped = true // if we're not ringing and it tries playing, it means the user opened the app somehow
- audioAttributes.setUsage(AudioAttributesCompat.USAGE_MEDIA)
- audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
- audioFocusRequest = audioFocusRequestBuilder.build()
- player.audioAttributes = com.google.android.exoplayer2.audio.AudioAttributes
- .Builder()
- .setContentType(C.CONTENT_TYPE_MUSIC)
- .setUsage(C.USAGE_MEDIA)
- .build()
- }
- // the old requestAudioFocus is deprecated on API26+. Using AudioManagerCompat library for consistent code across versions
- val result = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
- if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- return
- }
- if (mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_PLAYING && !isRinging && isAlarmStopped)
- {
- return //nothing to do here
- }
-
- PlayerStore.instance.playbackState.value = PlaybackStateCompat.STATE_PLAYING
-
- // Reinitialize media player. Otherwise the playback doesn't resume when beginPlaying. Dunno why.
- // Prepare the player with the source.
- if (isFallback)
- {
- player.prepare(fallbackMediaSource)
- player.repeatMode = ExoPlayer.REPEAT_MODE_ALL
- }
- else {
- player.prepare(radioMediaSource)
- player.repeatMode = ExoPlayer.REPEAT_MODE_OFF
- }
-
- // START PLAYBACK, LET'S ROCK
- player.playWhenReady = true
- nowPlayingNotification.update(this, isUpdatingNotificationButton = true, isRinging = isRinging)
-
- playbackStateBuilder.setState(
- PlaybackStateCompat.STATE_PLAYING,
- 0,
- 1.0f,
- SystemClock.elapsedRealtime()
- )
- mediaSession.setPlaybackState(playbackStateBuilder.build())
-
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "begin playing")
- }
-
- private fun pausePlaying()
- {
- stopPlaying()
- }
-
- // stop playing but keep the notification.
- fun stopPlaying()
- {
- if (mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_STOPPED)
- return // nothing to do here
- if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
- isAlarmStopped = true
-
- PlayerStore.instance.playbackState.value = PlaybackStateCompat.STATE_STOPPED
-
- // STOP THE PLAYBACK
- player.stop()
-
- nowPlayingNotification.update(this, true)
- playbackStateBuilder.setState(
- PlaybackStateCompat.STATE_STOPPED,
- 0,
- 1.0f,
- SystemClock.elapsedRealtime()
- )
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "stopped")
-
- mediaSession.setPlaybackState(playbackStateBuilder.build())
- }
-
- fun setVolume(vol: Int?) {
- var v = vol
- when(v)
- {
- null -> { player.volume = 0f ; return } // null means "mute"
- -1 -> v = PlayerStore.instance.volume.value // -1 means "restore previous volume"
- }
-
- // re-shaped volume setter with a logarithmic (ln) function.
- // I think it sounds more natural this way. Adjust coefficient to change the function shape.
- // visualize it on any graphic calculator if you're unsure.
- val c : Float = 2.toFloat()
- val x : Float = v!!.toFloat()/100
- player.volume = -(1/c)* ln(1-(1- exp(-c))*x)
- }
-
- private val mediaSessionCallback = object : MediaSessionCompat.Callback() {
- override fun onPlay() {
- beginPlaying()
- }
-
- override fun onPause() {
- pausePlaying()
- }
-
- override fun onStop() {
- stopPlaying()
- }
-
- override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
- // explicit handling of Media Buttons (for example bluetooth commands)
- // The hardware key on a corded headphones are handled in the MainActivity (for <API21)
- if (PlayerStore.instance.isServiceStarted.value!!) {
- val keyEvent =
- mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
- if (keyEvent == null || ((keyEvent.action) != KeyEvent.ACTION_DOWN)) {
- return false
- }
-
- when (keyEvent.keyCode) {
- KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
- //// Is this some kind of debouncing ? I'm not sure.
- //if (keyEvent.repeatCount > 0) {
- // return false
- //} else {
- if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
- pausePlaying()
- else
- beginPlaying()
- //}
- return true
- }
- KeyEvent.KEYCODE_MEDIA_STOP -> stopPlaying()
- KeyEvent.KEYCODE_MEDIA_PAUSE -> pausePlaying()
- KeyEvent.KEYCODE_MEDIA_PLAY -> beginPlaying()
- else -> return false // these actions are the only ones we acknowledge.
- }
-
- }
- return false
- }
- }
-
- private val exoPlayerEventListener = object : Player.EventListener {
- override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
- super.onPlayerStateChanged(playWhenReady, playbackState)
- numberOfSongs = 0
- var state = ""
- when(playbackState)
- {
- Player.STATE_BUFFERING -> state = "Player.STATE_BUFFERING"
- Player.STATE_IDLE -> {
- state = "Player.STATE_IDLE"
- // inform the PlayerStore that the playback has stopped. This enables the ticker, triggers API fetch, and updates UI in no-network state.
- if (PlayerStore.instance.playbackState.value != PlaybackStateCompat.STATE_STOPPED)
- {
- PlayerStore.instance.playbackState.postValue(PlaybackStateCompat.STATE_STOPPED)
- PlayerStore.instance.isPlaying.postValue(false)
- }
- }
- Player.STATE_ENDED -> state = "Player.STATE_ENDED"
- Player.STATE_READY -> state = "Player.STATE_READY"
- }
- //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
- }
- }
-
-
-
- }
|