저번 포스팅에 이어서 이번에는 실제로 간단한 포그라운드 서비스를 클라이언트와 연결하여 화면에 간단한 텍스트를 띄워볼려고 한다.
Manifest.xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
...
<service
android:name=".MyService"
android:foregroundServiceType="dataSync">
</service>
...
</application>
Manifest에서 포그라운드 서비스에 대한 권한을 요청한다.
포그라운드 서비스를 사용할 때는 Notification이 필수이기 때문에 해당 권한을 요청하고 안드로이드 14버전 이상부터 포그라운드 서비스를 사용할 때 foregroundServiceType을 명시하고 관련 권한을 요청해야 한다.
서비스 타입의 경우 사용할려는 서비스의 맞는 유형을 선택하면 된다
MyService.kt
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startForeground(1, createNotification())
job = CoroutineScope(Dispatchers.IO).launch {
while (true) {
val cnt = _count.value ?: 0
delay(1000L)
withContext(Dispatchers.Main) {
_count.value = cnt + 1
}
}
}
return START_STICKY
}
startForeground()함수를 통해서 서비스를 포그라운드 서비스로 시작하여 애플리케이션이 꺼져도 Notification을 통해서 사용자에게 서비스가 실행 중임을 알린다.
inner class MyBinder: Binder() {
fun getService(): MyService {
return this@MyService
}
}
Binder클래스를 확장한 클래스로 클라이언트에서 서비스 객체를 사용할 수 있게 해준다. 이를 통해 클라이언트에서 서비스의 메서드를 직접 호출할 수 있다.
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return MyBinder()
}
클라이언트가 서비스를 바인드할 때 해당 메소드 호출하며 여기선 MyBinder() 클래스 객체를 반환해준다.
override fun onDestroy() {
super.onDestroy()
job?.cancel()
_count.value = 0
}
현재 실행중인 코루틴의 job 객체가 있다면 해당 작업을 종료시키고, stopSelf()함수를 통해 현재 서비스를 종료한다.
MainActivity.kt
private fun makeServiceConnection(): ServiceConnection {
return object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as MyService.MyBinder
myService = binder.getService()
myService?.run {
count.observe(this@MainActivity) {
binding.tvService.text = "count: $it"
}
}
}
override fun onServiceDisconnected(name: ComponentName?) {
isStart = false
}
}
}
클라이언트가 서비스에 바인드할 때의 콜백을 제공한다. onServiceConnected()에서 서비스가 연결되면 호출되는 메서드로 서비스 객체를 받아와 해당 서비스 객체에 직접 접근할 수 있다.
onServiceDisconnected()의 경우 서비스의 연결이 비정상적으로 끊겼을 때 호출 되는 메서드이다.
private val serviceIntent by lazy { Intent(this, MyService::class.java) }
private val connection: ServiceConnection by lazy { makeServiceConnection() }
private fun startService() {
if (isStart.not()) {
startForegroundService(serviceIntent)
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
isStart = true
}
}
private fun stopService() {
if (isStart) {
stopService(serviceIntent)
unbindService(connection)
isStart = false
}
}
startForegroundService()를 통해 서비스를 포그라운드로 실행하며 서비스 클래스의 onStartCommand()메서드를 실행한다.
bindService()를 통해 클라이언트(액티비티)를 서비스 클래스에 연결한다.
stopService()를 통해 실행중인 서비스를 종료한다 해당 메서드가 호출되면 서비스는 onDestroy()를 실행하며 종료된다.
unbind()를 통해 현재 클라이언트와 연결되어 있는 서비스의 연결을 해제한다.
override fun onStart() {
super.onStart()
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
}
해당 액티비티가 onStop() 또는 onDestroy() 되어도 onStart()에서 서비스와 연결하여 앱을 껐다 켜도 클라이언트에 다시 서비스가 연결되어 서비스에서 계속해서 정보를 받아오도록했다.
onStop()에서 unbind()를 하지 않은 이유는 취소버튼을 통해서 unbind()를 이미 실행한 뒤 해당 액티비티가 onStop() 상태에 도달하게 되면 이미 서비스가 해제된 상태에서 다시 해제하려고 하기 때문에 에러가 발생한다.
이제는 실제 프로젝트에서 위치정보와 여러가지 정보들을 서비스를 통해 백그라운드에서 실행하고 해당 정보들을 클라이언트에 연결하여 사용자가 앱을 켜서 서비스와 연결된 화면에 들어왔을 때 서비스로부터 정보를 받아오는 기능을 구현하면 된다...