이 포스팅은 Music Player 재생의 간단한 로직을 FLO 앱 챌린지에 맞춰 개발한 내용에 대해 제 생각을 정리한 글입니다. 부족한 부분이 있으면 댓글로 알려주시면 감사하겠습니다.
멜론 어플을 사용한 유저라면 다른 어플을 사용 중에도 음악이 재생되는 것을 알 수 있을 것이다. 이는 음악이 Service에서 무한히 재생이 되고 있다는 뜻. 보통의 Service는 어떠한 값도 반환하지 않고 서비스를 종료하는 요청 외에 외부 서비스 내 함수에 접근할 수 없다. 그런데 음악이 재생되고 있는 도중 멜론 앱을 다시 키면 현재 재생되고 있는 곡의 정보를 바로 보여준다. 왜?
답은 Bound Services 이다.
Service와 Activity의 관계과 서버와 클라이언트 관계가 된 것처럼 Activity는 Service의 요청해서 결과 값을 받을 수 있고 Service는 요청을 처리한다. 일반적인 Bound Service는 클라이언트와 연결된 상태로만 존재하며 음악 재생처럼 무한히 어떠한 기능이 실행되길 원한다면 onStartCommand()을 구현해야 한다. 다만 시스템에서 서비스의 종료를 관리해주지 못해서 종료해주는 코드를 넣어주어야 한다.
Music Player 구현
class MusicService : Service() {
var player : MediaPlayer? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.let {
val position = it.getIntExtra("position",0)
val urlPath = it.getStringExtra("mp3")
if(player == null)player = MediaPlayer.create(applicationContext,Uri.parse(urlPath))
player?.let { player ->
player.setOnCompletionListener { _ -> stopSelf() }
player.seekTo(position)
player.start()
}
}
return START_NOT_STICKY
}
...
override fun onDestroy() { //서비스 종료
super.onDestroy()
player?.release()
player = null
}
}
startService 함수를 Activity에서 호출하면 최초에는 onCreate() - onStartCommand() 순서로 실행된다. 또 반복해서 호출한다면 onStartCommand() 함수만 반복적으로 실행한다는 것을 유의하자.
필자는 startService 함수를 호출할 때 넘긴 Intent 객체에 음악의 재생 포지션과 음악 url을 넣어서 보내주었기 때문에 onCreate가 아닌 onStartCommand() 함수에서 MusicPlayer를 초기화해주었다.
무한히 실행되는 Service는 종료되는 시점을 Service 내 또는 Activity에서 명시해주어야 한다. 필자는 Music이 종료되는 시점에 서비스도 종료가 되도록 setOnCompletionListenr를 사용해서 Service가 스스로 종료하게 해 주었다. 스스로 종료하는 함수는 stopSelf()
Service가 종료가 되면 onDestroy가 실행된다. 이때는 MusicPlayer의 메모리를 해제해줘야 한다.
서비스 내 음악 관련 함수들 Bound Service로 접근
class MusicService : Service() {
...
fun musicTimeSet(time:Int){ //음악 원하는 구간으로 이동
player?.let{
it.pasue()
it.seekTo(time)
it.start()
}
}
fun musicStop(){ //음악 멈추기
player?.pause()
}
fun MusicStart(){ //음악 재생
player?.start()
}
}
Service에 MusicPlayer 객체가 있기 때문에 관련 함수를 public으로 선언한다. 이 public 함수를 접근하기 위해 Bound Service로 만들어주자
class MusicService : Service() {
...
private val binder = LocalBinder()
override fun onBind(intent: Intent?): IBinder? {
return binder
}
inner class LocalBinder : Binder() {
// Return this instance of LocalService so clients can call public methods
fun getService(): MusicService = this@MusicService
}
}
Bound Service는 우선 onBind을 오버라이드 해주어야 한다. 이 함수에서 반환하는 IBinder 객체가 클라이언트(ex Activity)가 서버(Service)에 요청할 수 있게 도와주는 인터페이스이다. 이로서 클라이언트가 Service의 public 함수들을 접근할 수 있게 된다.
Activity
class MainActivity : AppCompatActivity(){
lateinit var mService : MusicService
private var mBound: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as MusicService.LocalBinder
mService = binder.getService()
mBound = true
mService.player?.let {
//MusicPlayer 변수의 직접 접근 정보 가져올 수 있다.
}
}
override fun onServiceDisconnected(arg0: ComponentName) {
//서비스 연결 끊어짐 관련 처리
mBound = false
}
}
//앱을 실행했을 때, 이미 노래가 재생중이면 해당 서비스의 정보를 받아온다.
override fun onStart() {
super.onStart()
Intent(this,MusicService::class.java).also {
intent -> bindService(intent,connection,Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
mBound = false
}
}
클라이언트는 Service를 저장할 변수를 선언한다.
onStart()와 onStop() 콜백 함수들에서는 Bound Service의 연결, 해제가 진행되어야 한다. 연결하는 함수는 bindService(...)로 반환되는 IBinder를 수신하기 위해 ServiceConnection 생성해서 전달해준다.
다만 주의할 점은 ServiceConnection은 비동기로 실행이 되기 때문에 onCreate에서 음악에 대한 정보를 처리할 때 아직 결과를 못 받았을 경우를 고려해야 한다.
mService.musicStart()
mService.musicStop()
바인딩이 완료되었다면 위처럼 public 함수들에 접근할 수 있다.
'Android > Common' 카테고리의 다른 글
[Android] 내부 DB - Room (0) | 2020.06.03 |
---|---|
[Android] Google Calendar API 사용법 (4) | 2020.05.02 |
[Android] DataBinding 정리 (0) | 2020.04.25 |
[Android] 로또 정보 크롤링해보기 by Kotlin (4) | 2020.04.08 |
[Android] Firebase ML Kit - Translate에 대한 소개 (6) | 2020.03.29 |