There is the fragment (AudioFragment.kt) in which the service (AudioService.kt) with the ExoPlayer
‘s instance starts:
class AudioFragment : Fragment() {
private lateinit var urlFromAPI: String
private var wasButtonClicked: Boolean = false
private var isBounded = false
private var audioService: AudioService? = null
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, iBinder: IBinder?) {
val localBinder = iBinder as AudioService.AudioBinder
audioService = localBinder.getService()
isBounded = true
urlFromAPI = activity?.intent?.getStringExtra("audio").toString()
startAudioService()
}
override fun onServiceDisconnected(name: ComponentName?) {
isBounded = false
audioService = null
stopAudioService()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAudioBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.tvAudioPosition.text = "00:00"
bindToAudioService()
binding.btnAudioOff.setOnClickListener {
if (wasButtonClicked) {
binding.btnAudioOff.setImageResource(R.drawable.baseline_pause_24)
wasButtonClicked = false
playAgainAudio()
} else {
binding.btnAudioOff.setImageResource(R.drawable.baseline_play_arrow_24)
wasButtonClicked = true
pauseAudio()
}
}
}
private fun unbindToAudioService() {
activity?.unbindService(serviceConnection)
}
private fun bindToAudioService() {
val intent = Intent(activity, AudioService::class.java)
activity?.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
private fun startAudioService() {
val serviceIntent = Intent(activity, AudioService::class.java)
val dataFromAPI = activity?.intent?.getStringExtra("data").toString()
val titleFromAPI = activity?.intent?.getStringExtra("title").toString()
val urlFromAPI = activity?.intent?.getStringExtra("audio").toString()
serviceIntent.putExtra("data", dataFromAPI)
serviceIntent.putExtra("title", titleFromAPI)
serviceIntent.putExtra("audio", urlFromAPI)
serviceIntent.action = Constants.ACTION.STARTFOREGROUND_ACTION
activity?.startService(serviceIntent)
}
private fun pauseAudio() {
val serviceIntent = Intent(activity, AudioService::class.java)
serviceIntent.action = Constants.ACTION.PAUSEFOREGROUND_ACTION
activity?.startService(serviceIntent)
}
private fun playAgainAudio() {
val serviceIntent = Intent(activity, AudioService::class.java)
serviceIntent.action = Constants.ACTION.PLAYAGAINFOREGROUND_ACTION
activity?.startService(serviceIntent)
}
private fun stopAudioService() {
val serviceIntent = Intent(activity, AudioService::class.java)
activity?.stopService(serviceIntent)
}
override fun onDestroy() {
stopAudioService()
unbindToAudioService()
super.onDestroy()
}
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var binding: FragmentAudioBinding
@JvmStatic
fun newInstance() = AudioFragment()
}
}
AudioService.kt:
class AudioService : Service() {
private lateinit var dataFromAPI: String
private lateinit var titleFromAPI: String
private lateinit var urlFromAPI: String
var mediaPlayer: ExoPlayer? = null
var duration: Int = 0
var currentPosition: Int = 0
private val audioBinder = AudioBinder()
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("ForegroundServiceType")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
dataFromAPI = intent?.getStringExtra("data").toString()
titleFromAPI = intent?.getStringExtra("title").toString()
urlFromAPI = intent?.getStringExtra("audio").toString()
when (intent?.action) {
Constants.ACTION.STARTFOREGROUND_ACTION -> {
initialiseAudio(urlFromAPI)
startForeground(NOTIFICATION_ID, createNotification())
initialiseSeekBar()
}
Constants.ACTION.PAUSEFOREGROUND_ACTION -> {
pauseAudio()
startForeground(NOTIFICATION_ID, createNotification())
}
Constants.ACTION.PLAYAGAINFOREGROUND_ACTION -> {
playAudio()
startForeground(NOTIFICATION_ID, createNotification())
}
Constants.ACTION.STOPFOREGROUND_ACTION -> {
stopForeground(true)
pauseAudio()
stopSelf()
}
}
return START_NOT_STICKY
}
private fun initialiseAudio(urlFromAPI: String) {
mediaPlayer = ExoPlayer.Builder(this).build()
mediaPlayer!!.setMediaItem(MediaItem.fromUri(urlFromAPI))
try {
mediaPlayer!!.prepare()
mediaPlayer!!.play()
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun playAudio() {
if (!isPlaying())
mediaPlayer!!.play()
}
private fun pauseAudio() {
if (isPlaying())
mediaPlayer!!.pause()
}
override fun onDestroy() {
mediaPlayer!!.playWhenReady = false
mediaPlayer!!.stop()
mediaPlayer!!.release()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder {
return audioBinder
}
inner class AudioBinder : Binder() {
fun getService(): AudioService {
return this@AudioService
}
}
@OptIn(UnstableApi::class)
private fun initialiseSeekBar() {
mediaPlayer!!.addListener(object : Player.Listener {
@SuppressLint("UseCompatLoadingForDrawables")
@Deprecated("Deprecated in Java")
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState == Player.STATE_READY && mediaPlayer!!.playWhenReady) {
binding.btnAudioOff.setImageDrawable(resources.getDrawable(R.drawable.baseline_pause_24))
} else {
binding.btnAudioOff.setImageDrawable(resources.getDrawable(R.drawable.baseline_play_arrow_24))
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
duration = mediaPlayer!!.duration.toInt() / 1000
binding.skAudioSeekBar.max = duration
binding.tvAudioDuration.text = getTimeString(duration)
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
currentPosition = mediaPlayer!!.currentPosition.toInt() / 1000
binding.skAudioSeekBar.progress = currentPosition
binding.tvAudioPosition.text = getTimeString(currentPosition)
binding.tvAudioDuration.text = getTimeString(mediaPlayer!!.duration.toInt() / 1000)
}
})
binding.skAudioSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
mediaPlayer!!.seekTo(progress.toLong() * 1000)
binding.tvAudioPosition.text = getTimeString(progress)
binding.tvAudioDuration.text = getTimeString(duration)
}
}
override fun onStartTrackingTouch(p0: SeekBar?) {
}
override fun onStopTrackingTouch(p0: SeekBar?) {
}
})
val handler = Handler(Looper.getMainLooper())
handler.post(object : Runnable {
override fun run() {
currentPosition = mediaPlayer!!.currentPosition.toInt() / 1000
binding.skAudioSeekBar.progress = currentPosition
binding.tvAudioPosition.text = getTimeString(currentPosition)
binding.tvAudioDuration.text = getTimeString(duration)
handler.postDelayed(this, 1000)
}
})
}
@SuppressLint("DefaultLocale")
fun getTimeString(duration: Int): String {
val min = duration / 60
val sec = duration % 60
val time = String.format("%02d:%02d", min, sec)
return time
}
@OptIn(UnstableApi::class)
@Suppress("DEPRECATED_IDENTITY_EQUALS")
private fun isPlaying(): Boolean {
return mediaPlayer!!.playbackState === Player.STATE_READY && mediaPlayer!!.playWhenReady
}
}
When the AudioService.kt is called for the first time, everything works fine, but when you call it again, two TextView
with the position and duration of the audio track begin to “blink”: first (for a second) they show the position and duration relevant for the previous service launch, and then (for a second) – for the current one. Together with them, the SeekBar
“jumps”.
I really can’t understand how and why the system saves the ExoPlayer
‘s position and duration from the already destroyed service (even after restarting the app).