RadioService.kt 29KB

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