RadioService.kt 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package fr.forum_thalie.tsumugi
  2. import android.app.Service
  3. import android.content.BroadcastReceiver
  4. import android.content.Context
  5. import android.content.Intent
  6. import android.content.IntentFilter
  7. import android.graphics.Bitmap
  8. import android.support.v4.media.MediaBrowserCompat
  9. import android.util.Log
  10. import androidx.media.MediaBrowserServiceCompat
  11. import android.media.AudioManager
  12. import android.os.*
  13. import android.support.v4.media.session.MediaSessionCompat
  14. import android.support.v4.media.session.PlaybackStateCompat
  15. import androidx.media.session.MediaButtonReceiver
  16. import com.google.android.exoplayer2.source.ProgressiveMediaSource
  17. import com.google.android.exoplayer2.util.Util.getUserAgent
  18. import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
  19. import android.net.Uri
  20. import android.support.v4.media.MediaMetadataCompat
  21. import android.telephony.PhoneStateListener
  22. import android.telephony.TelephonyManager
  23. import android.view.KeyEvent
  24. import androidx.core.app.NotificationCompat
  25. import androidx.core.content.edit
  26. import androidx.lifecycle.Observer
  27. import androidx.media.AudioAttributesCompat
  28. import androidx.media.AudioFocusRequestCompat
  29. import androidx.media.AudioManagerCompat
  30. import androidx.preference.PreferenceManager
  31. import com.google.android.exoplayer2.*
  32. import com.google.android.exoplayer2.metadata.icy.*
  33. import fr.forum_thalie.tsumugi.alarm.RadioAlarm
  34. import fr.forum_thalie.tsumugi.alarm.RadioSleeper
  35. import fr.forum_thalie.tsumugi.planning.Planning
  36. import fr.forum_thalie.tsumugi.playerstore.PlayerStore
  37. import java.util.*
  38. import kotlin.math.exp
  39. import kotlin.math.ln
  40. import kotlin.system.exitProcess
  41. class RadioService : MediaBrowserServiceCompat() {
  42. private val radioTag = "======RadioService====="
  43. private lateinit var nowPlayingNotification: NowPlayingNotification
  44. private val radioServiceId = 1
  45. private var numberOfSongs = 0
  46. private val apiTicker: Timer = Timer()
  47. private var isAlarmStopped: Boolean = false
  48. // Define the broadcast receiver to handle any broadcasts
  49. private val receiver = object : BroadcastReceiver() {
  50. override fun onReceive(context: Context, intent: Intent) {
  51. val action = intent.action
  52. if (action != null && action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
  53. val i = Intent(context, RadioService::class.java)
  54. i.putExtra("action", Actions.STOP.name)
  55. context.startService(i)
  56. }
  57. if (action != null && action == Intent.ACTION_HEADSET_PLUG)
  58. {
  59. var headsetPluggedIn = false
  60. // In the Intent state there's the value whether the headphones are plugged or not.
  61. // This *should* work in any case...
  62. when (intent.getIntExtra("state", -1)) {
  63. 0 -> {
  64. Log.d(tag, radioTag + "Headset is unplugged")
  65. }
  66. 1 -> {
  67. Log.d(tag, radioTag + "Headset is plugged")
  68. headsetPluggedIn = true
  69. }
  70. else -> {
  71. Log.d(tag, radioTag + "I have no idea what the headset state is")
  72. }
  73. }
  74. /*
  75. val am = getSystemService(AUDIO_SERVICE) as AudioManager
  76. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
  77. {
  78. val devices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
  79. for (d in devices)
  80. {
  81. if (d.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || d.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
  82. headsetPluggedIn = true
  83. }
  84. }
  85. else
  86. {
  87. Log.d(tag, radioTag + "Can't get state?")
  88. }
  89. */
  90. if((mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_STOPPED
  91. || mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_PAUSED)
  92. && headsetPluggedIn)
  93. beginPlaying()
  94. }
  95. }
  96. }
  97. // ##################################################
  98. // ################### OBSERVERS ####################
  99. // ##################################################
  100. private val volumeObserver: Observer<Int> = Observer {
  101. setVolume(it)
  102. }
  103. private val isMutedObserver: Observer<Boolean> = Observer {
  104. setVolume(if (it) null else -1)
  105. }
  106. private val isPlayingObserver: Observer<Boolean> = Observer {
  107. if (it)
  108. beginPlaying()
  109. else
  110. stopPlaying()
  111. }
  112. private val titleObserver = Observer<String> {
  113. // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
  114. if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
  115. && it != noConnectionValue)
  116. {
  117. PlayerStore.instance.updateLp()
  118. }
  119. nowPlayingNotification.update(this)
  120. Planning.instance.checkProgramme()
  121. }
  122. private val streamerPictureObserver = Observer<Bitmap> {
  123. nowPlayingNotification.update(this)
  124. }
  125. // ##################################################
  126. // ############## LIFECYCLE CALLBACKS ###############
  127. // ##################################################
  128. override fun onLoadChildren(
  129. parentId: String,
  130. result: Result<MutableList<MediaBrowserCompat.MediaItem>>
  131. ) {
  132. result.sendResult(null)
  133. }
  134. override fun onGetRoot(
  135. clientPackageName: String,
  136. clientUid: Int,
  137. rootHints: Bundle?
  138. ): BrowserRoot? {
  139. // Clients can connect, but you can't browse internet radio
  140. // so onLoadChildren returns nothing. This disables the ability to browse for content.
  141. return BrowserRoot(getString(R.string.MEDIA_ROOT_ID), null)
  142. }
  143. override fun onCreate() {
  144. super.onCreate()
  145. preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
  146. // Define managers
  147. telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
  148. telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
  149. audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
  150. //define the audioFocusRequest
  151. val audioFocusRequestBuilder = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
  152. audioFocusRequestBuilder.setOnAudioFocusChangeListener(focusChangeListener)
  153. val audioAttributes = AudioAttributesCompat.Builder()
  154. audioAttributes.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
  155. audioAttributes.setUsage(AudioAttributesCompat.USAGE_MEDIA)
  156. audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
  157. audioFocusRequest = audioFocusRequestBuilder.build()
  158. // This stuff is for the broadcast receiver
  159. val filter = IntentFilter()
  160. filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
  161. filter.addAction(Intent.ACTION_HEADSET_PLUG)
  162. registerReceiver(receiver, filter)
  163. // setup media player
  164. setupMediaPlayer()
  165. createMediaSession()
  166. nowPlayingNotification = NowPlayingNotification(
  167. notificationChannelId = this.getString(R.string.nowPlayingChannelId),
  168. notificationChannel = R.string.nowPlayingNotificationChannel,
  169. notificationId = 1,
  170. notificationImportance = NotificationCompat.PRIORITY_LOW
  171. )
  172. nowPlayingNotification.create(this, mediaSession)
  173. PlayerStore.instance.currentSong.title.observeForever(titleObserver)
  174. PlayerStore.instance.volume.observeForever(volumeObserver)
  175. PlayerStore.instance.isPlaying.observeForever(isPlayingObserver)
  176. PlayerStore.instance.isMuted.observeForever(isMutedObserver)
  177. PlayerStore.instance.streamerPicture.observeForever(streamerPictureObserver)
  178. startForeground(radioServiceId, nowPlayingNotification.notification)
  179. PlayerStore.instance.isServiceStarted.value = true
  180. Log.d(tag, radioTag + "created")
  181. }
  182. private val handler = Handler()
  183. class LowerVolumeRunnable : Runnable {
  184. override fun run() {
  185. PlayerStore.instance.volume.postValue(
  186. (PlayerStore.instance.volume.value!!.toFloat() * (9f / 10f)).toInt()
  187. ) // the setVolume is called by the volumeObserver in RadioService (on main thread for ExoPlayer!)
  188. }
  189. }
  190. private val lowerVolumeRunnable = LowerVolumeRunnable()
  191. override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  192. if (intent?.getStringExtra("action") == null)
  193. return super.onStartCommand(intent, flags, startId)
  194. if (MediaButtonReceiver.handleIntent(mediaSession, intent) != null)
  195. return super.onStartCommand(intent, flags, startId)
  196. when (intent.getStringExtra("action")) {
  197. Actions.PLAY.name -> beginPlaying()
  198. 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
  199. Actions.PAUSE.name -> { setVolume(PlayerStore.instance.volume.value); pausePlaying() }
  200. Actions.VOLUME.name -> setVolume(intent.getIntExtra("value", 100))
  201. Actions.KILL.name -> {stopForeground(true); stopSelf(); return Service.START_NOT_STICKY}
  202. Actions.NOTIFY.name -> nowPlayingNotification.update(this)
  203. Actions.PLAY_OR_FALLBACK.name -> beginPlayingOrFallback()
  204. Actions.FADE_OUT.name -> {
  205. for (i in 1 until 28) // we schedule 28 "LowerVolumeRunnable" every 2 seconds (i * 2)
  206. {
  207. // I couldn't find how to send multiple times the same PendingIntent using AlarmManager, so I relied on Handler instead.
  208. // I think there's no guarantee of exact time with the Handler, especially when the device is in deep sleep,
  209. // But when I force-set the Deep Sleep mode with ADB, it worked fine, so I'll leave it as this.
  210. // BUT! SOMETIMES IT DIDN'T WORK AND I DON'T KNOW WHY.
  211. // I hope that moving the handler in the RadioService would solve the issue and trigger it correctly.
  212. handler.postDelayed(lowerVolumeRunnable, (i * 2 * 1000).toLong())
  213. }
  214. }
  215. Actions.CANCEL_FADE_OUT.name -> { handler.removeCallbacks(lowerVolumeRunnable) }
  216. Actions.SNOOZE.name -> { RadioAlarm.instance.snooze(this) }
  217. }
  218. Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
  219. super.onStartCommand(intent, flags, startId)
  220. // 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.
  221. return START_STICKY
  222. }
  223. override fun onTaskRemoved(rootIntent: Intent) {
  224. if (mediaSession.controller.playbackState.state != PlaybackStateCompat.STATE_PLAYING) {
  225. nowPlayingNotification.clear()
  226. stopSelf()
  227. }
  228. super.onTaskRemoved(rootIntent)
  229. Log.d(tag, radioTag + "task removed")
  230. }
  231. override fun onDestroy() {
  232. super.onDestroy()
  233. player.stop()
  234. player.release()
  235. unregisterReceiver(receiver)
  236. PlayerStore.instance.currentSong.title.removeObserver(titleObserver)
  237. PlayerStore.instance.volume.removeObserver(volumeObserver)
  238. PlayerStore.instance.isPlaying.removeObserver(isPlayingObserver)
  239. PlayerStore.instance.isMuted.removeObserver(isMutedObserver)
  240. PlayerStore.instance.streamerPicture.removeObserver(streamerPictureObserver)
  241. mediaSession.isActive = false
  242. mediaSession.setMediaButtonReceiver(null)
  243. mediaSession.release()
  244. PlayerStore.instance.isServiceStarted.value = false
  245. PlayerStore.instance.isInitialized = false
  246. RadioSleeper.instance.cancelSleep(this, isClosing = true)
  247. PreferenceManager.getDefaultSharedPreferences(this).edit {
  248. this.putBoolean("isSleeping", false)
  249. this.commit()
  250. }
  251. apiTicker.cancel() // stops the timer.
  252. Log.d(tag, radioTag + "destroyed")
  253. // if the service is destroyed, the application had become useless.
  254. exitProcess(0)
  255. }
  256. // ########################################
  257. // ######## AUDIO FOCUS MANAGEMENT ########
  258. //#########################################
  259. // Define the managers
  260. private var telephonyManager: TelephonyManager? = null
  261. private lateinit var audioManager: AudioManager
  262. private lateinit var audioFocusRequest: AudioFocusRequestCompat
  263. private val phoneStateListener = object : PhoneStateListener() {
  264. override fun onCallStateChanged(state: Int, incomingNumber: String) {
  265. super.onCallStateChanged(state, incomingNumber)
  266. if (state != TelephonyManager.CALL_STATE_IDLE) {
  267. setVolume(0)
  268. } else {
  269. setVolume(PlayerStore.instance.volume.value!!)
  270. }
  271. }
  272. }
  273. // Define the listener that will control what happens when focus is changed
  274. private val focusChangeListener =
  275. AudioManager.OnAudioFocusChangeListener { focusChange ->
  276. when (focusChange) {
  277. AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> setVolume((0.20f * PlayerStore.instance.volume.value!!).toInt()) //20% of current volume.
  278. AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> setVolume(0)
  279. AudioManager.AUDIOFOCUS_LOSS -> stopPlaying()
  280. AudioManager.AUDIOFOCUS_GAIN -> setVolume(PlayerStore.instance.volume.value!!)
  281. else -> {}
  282. }
  283. }
  284. // ########################################
  285. // ######## MEDIA PLAYER / SESSION ########
  286. // ########################################
  287. private lateinit var mediaSession : MediaSessionCompat
  288. private lateinit var playbackStateBuilder: PlaybackStateCompat.Builder
  289. private lateinit var metadataBuilder: MediaMetadataCompat.Builder
  290. private lateinit var player: SimpleExoPlayer
  291. private lateinit var radioMediaSource: ProgressiveMediaSource
  292. private lateinit var fallbackMediaSource: ProgressiveMediaSource
  293. private fun setupMediaPlayer(){
  294. val minBufferMillis = 15 * 1000 // Default value
  295. val maxBufferMillis = 50 * 1000 // Default value
  296. val bufferForPlayback = 4 * 1000 // Default is 2.5s.
  297. // Increasing it makes it more robust to short connection loss, at the expense of latency when we press Play. 4s seems reasonable to me.
  298. val bufferForPlaybackAfterRebuffer = 7 * 1000 // Default is 5s.
  299. val loadControl = DefaultLoadControl.Builder().apply {
  300. setBufferDurationsMs(minBufferMillis, maxBufferMillis, bufferForPlayback, bufferForPlaybackAfterRebuffer)
  301. }.createDefaultLoadControl()
  302. val playerBuilder = SimpleExoPlayer.Builder(this)
  303. playerBuilder.setLoadControl(loadControl)
  304. player = playerBuilder.build()
  305. player.addMetadataOutput {
  306. for (i in 0 until it.length()) {
  307. val entry = it.get(i)
  308. if (entry is IcyHeaders) {
  309. Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
  310. }
  311. if (entry is IcyInfo) {
  312. Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
  313. // Note : Kotlin supports UTF-8 by default.
  314. numberOfSongs++
  315. val data = entry.title!!
  316. PlayerStore.instance.currentSong.setTitleArtist(data)
  317. }
  318. val d : Long = ((PlayerStore.instance.currentSong.stopTime.value?.minus(PlayerStore.instance.currentSong.startTime.value!!) ?: 0) / 1000)
  319. val duration = if (d > 0) d - (PlayerStore.instance.latencyCompensator) else 0
  320. metadataBuilder.putString(
  321. MediaMetadataCompat.METADATA_KEY_TITLE,
  322. PlayerStore.instance.currentSong.title.value
  323. )
  324. .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, PlayerStore.instance.currentSong.artist.value)
  325. .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
  326. mediaSession.setMetadata(metadataBuilder.build())
  327. val intent = Intent("com.android.music.metachanged")
  328. intent.putExtra("artist", PlayerStore.instance.currentSong.artist.value)
  329. intent.putExtra("track", PlayerStore.instance.currentSong.title.value)
  330. intent.putExtra("duration", duration)
  331. intent.putExtra("position", 0)
  332. sendBroadcast(intent)
  333. }
  334. }
  335. // this listener allows to reset numberOfSongs if the connection is lost.
  336. player.addListener(exoPlayerEventListener)
  337. // Produces DataSource instances through which media data is loaded.
  338. val dataSourceFactory = DefaultDataSourceFactory(
  339. this,
  340. getUserAgent(this, getString(R.string.app_name))
  341. )
  342. // This is the MediaSource representing the media to be played.
  343. radioMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
  344. .createMediaSource(Uri.parse(getString(R.string.STREAM_URL_TSUMUGI)))
  345. fallbackMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
  346. .createMediaSource(Uri.parse("file:///android_asset/the_stream_is_down.mp3"))
  347. }
  348. private fun createMediaSession() {
  349. mediaSession = MediaSessionCompat(this, "RadioMediaSession")
  350. // Deprecated flags
  351. // mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS and MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
  352. mediaSession.isActive = true
  353. mediaSession.setCallback(mediaSessionCallback)
  354. playbackStateBuilder = PlaybackStateCompat.Builder()
  355. playbackStateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
  356. .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f, SystemClock.elapsedRealtime())
  357. metadataBuilder = MediaMetadataCompat.Builder()
  358. mediaSession.setPlaybackState(playbackStateBuilder.build())
  359. }
  360. // ########################################
  361. // ######### SERVICE START/STOP ###########
  362. // ########################################
  363. // this function is playing the stream if available, or a default sound if there's a problem.
  364. private fun beginPlayingOrFallback()
  365. {
  366. PlayerStore.instance.volume.value = 100 // we set the max volume for exoPlayer to be sure it rings correctly.
  367. beginPlaying(isRinging = true, isFallback = false)
  368. val wait: (Any?) -> Any = {
  369. /*
  370. 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!!)
  371. If the player stops the alarm (by calling an intent), the isAlarmStopped flag will be raised.
  372. */
  373. isAlarmStopped = false // reset the flag
  374. var i = 0
  375. while (i < 17)
  376. {
  377. Thread.sleep(1000)
  378. i++
  379. Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
  380. }
  381. }
  382. val post: (Any?) -> Unit = {
  383. // we verify : if the player is not playing, and if the user didn't stop it, it means that there's a network issue.
  384. // So we use the fallback sound to wake up the user!!
  385. // (note: player.isPlaying is only accessible on main thread, so we can't check in the wait() lambda)
  386. if (!player.isPlaying && !isAlarmStopped)
  387. beginPlaying(isRinging = true, isFallback = true)
  388. }
  389. Async(wait, post)
  390. }
  391. fun beginPlaying(isRinging: Boolean = false, isFallback: Boolean = false)
  392. {
  393. //define the audioFocusRequest
  394. val audioFocusRequestBuilder = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
  395. audioFocusRequestBuilder.setOnAudioFocusChangeListener(focusChangeListener)
  396. val audioAttributes = AudioAttributesCompat.Builder()
  397. audioAttributes.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
  398. if (isRinging)
  399. {
  400. audioAttributes.setUsage(AudioAttributesCompat.USAGE_ALARM)
  401. audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
  402. audioFocusRequest = audioFocusRequestBuilder.build()
  403. player.audioAttributes = com.google.android.exoplayer2.audio.AudioAttributes
  404. .Builder()
  405. .setContentType(C.CONTENT_TYPE_MUSIC)
  406. .setUsage(C.USAGE_ALARM)
  407. .build()
  408. } else {
  409. audioAttributes.setUsage(AudioAttributesCompat.USAGE_MEDIA)
  410. audioFocusRequestBuilder.setAudioAttributes(audioAttributes.build())
  411. audioFocusRequest = audioFocusRequestBuilder.build()
  412. player.audioAttributes = com.google.android.exoplayer2.audio.AudioAttributes
  413. .Builder()
  414. .setContentType(C.CONTENT_TYPE_MUSIC)
  415. .setUsage(C.USAGE_MEDIA)
  416. .build()
  417. }
  418. // the old requestAudioFocus is deprecated on API26+. Using AudioManagerCompat library for consistent code across versions
  419. val result = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
  420. if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
  421. return
  422. }
  423. if (mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_PLAYING && !isRinging)
  424. return // nothing to do here
  425. PlayerStore.instance.playbackState.value = PlaybackStateCompat.STATE_PLAYING
  426. // Reinitialize media player. Otherwise the playback doesn't resume when beginPlaying. Dunno why.
  427. // Prepare the player with the source.
  428. if (isFallback)
  429. {
  430. player.prepare(fallbackMediaSource)
  431. player.repeatMode = ExoPlayer.REPEAT_MODE_ALL
  432. }
  433. else {
  434. player.prepare(radioMediaSource)
  435. player.repeatMode = ExoPlayer.REPEAT_MODE_OFF
  436. }
  437. // START PLAYBACK, LET'S ROCK
  438. player.playWhenReady = true
  439. nowPlayingNotification.update(this, isUpdatingNotificationButton = true, isRinging = isRinging)
  440. playbackStateBuilder.setState(
  441. PlaybackStateCompat.STATE_PLAYING,
  442. 0,
  443. 1.0f,
  444. SystemClock.elapsedRealtime()
  445. )
  446. mediaSession.setPlaybackState(playbackStateBuilder.build())
  447. Log.d(tag, radioTag + "begin playing")
  448. }
  449. private fun pausePlaying()
  450. {
  451. stopPlaying()
  452. }
  453. // stop playing but keep the notification.
  454. fun stopPlaying()
  455. {
  456. if (mediaSession.controller.playbackState.state == PlaybackStateCompat.STATE_STOPPED)
  457. return // nothing to do here
  458. if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
  459. isAlarmStopped = true
  460. PlayerStore.instance.playbackState.value = PlaybackStateCompat.STATE_STOPPED
  461. // STOP THE PLAYBACK
  462. player.stop()
  463. nowPlayingNotification.update(this, true)
  464. playbackStateBuilder.setState(
  465. PlaybackStateCompat.STATE_STOPPED,
  466. 0,
  467. 1.0f,
  468. SystemClock.elapsedRealtime()
  469. )
  470. Log.d(tag, radioTag + "stopped")
  471. mediaSession.setPlaybackState(playbackStateBuilder.build())
  472. }
  473. fun setVolume(vol: Int?) {
  474. var v = vol
  475. when(v)
  476. {
  477. null -> { player.volume = 0f ; return } // null means "mute"
  478. -1 -> v = PlayerStore.instance.volume.value // -1 means "restore previous volume"
  479. }
  480. // re-shaped volume setter with a logarithmic (ln) function.
  481. // I think it sounds more natural this way. Adjust coefficient to change the function shape.
  482. // visualize it on any graphic calculator if you're unsure.
  483. val c : Float = 2.toFloat()
  484. val x : Float = v!!.toFloat()/100
  485. player.volume = -(1/c)* ln(1-(1- exp(-c))*x)
  486. }
  487. private val mediaSessionCallback = object : MediaSessionCompat.Callback() {
  488. override fun onPlay() {
  489. beginPlaying()
  490. }
  491. override fun onPause() {
  492. pausePlaying()
  493. }
  494. override fun onStop() {
  495. stopPlaying()
  496. }
  497. override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
  498. // explicit handling of Media Buttons (for example bluetooth commands)
  499. // The hardware key on a corded headphones are handled in the MainActivity (for <API21)
  500. if (PlayerStore.instance.isServiceStarted.value!!) {
  501. val keyEvent =
  502. mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
  503. if (keyEvent == null || ((keyEvent.action) != KeyEvent.ACTION_DOWN)) {
  504. return false
  505. }
  506. when (keyEvent.keyCode) {
  507. KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
  508. //// Is this some kind of debouncing ? I'm not sure.
  509. //if (keyEvent.repeatCount > 0) {
  510. // return false
  511. //} else {
  512. if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
  513. pausePlaying()
  514. else
  515. beginPlaying()
  516. //}
  517. return true
  518. }
  519. KeyEvent.KEYCODE_MEDIA_STOP -> stopPlaying()
  520. KeyEvent.KEYCODE_MEDIA_PAUSE -> pausePlaying()
  521. KeyEvent.KEYCODE_MEDIA_PLAY -> beginPlaying()
  522. else -> return false // these actions are the only ones we acknowledge.
  523. }
  524. }
  525. return false
  526. }
  527. }
  528. private val exoPlayerEventListener = object : Player.EventListener {
  529. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
  530. super.onPlayerStateChanged(playWhenReady, playbackState)
  531. numberOfSongs = 0
  532. var state = ""
  533. when(playbackState)
  534. {
  535. Player.STATE_BUFFERING -> state = "Player.STATE_BUFFERING"
  536. Player.STATE_IDLE -> {
  537. state = "Player.STATE_IDLE"
  538. // inform the PlayerStore that the playback has stopped. This enables the ticker, triggers API fetch, and updates UI in no-network state.
  539. if (PlayerStore.instance.playbackState.value != PlaybackStateCompat.STATE_STOPPED)
  540. {
  541. PlayerStore.instance.playbackState.postValue(PlaybackStateCompat.STATE_STOPPED)
  542. PlayerStore.instance.isPlaying.postValue(false)
  543. }
  544. }
  545. Player.STATE_ENDED -> state = "Player.STATE_ENDED"
  546. Player.STATE_READY -> state = "Player.STATE_READY"
  547. }
  548. Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
  549. }
  550. }
  551. }