RadioService.kt 29KB

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