-
Notifications
You must be signed in to change notification settings - Fork 5
Activity와 Service 상호작용하기
약속 상세 화면에서 위치 공유를 ON/OFF를 할 수 있다. 위치공유는 포그라운드 서비스로 동작하기 때문에 activity와 service가 상호작용하는방법에 대해 생각해봐야했다.
서비스를 시작 후 bindService()
를 통해 클라이언트가 서비스에 바인딩 되도록 할 수 있다.
서비스가 시작되고 바인드되면 모든 클라이언트가 바인딩을 해제해도 서비스가 소멸되지 않는다.
클라이언트는 ServiceConnection
을 통해 서비스의 상태를 모니터링 할 수 있다.
bindService()
을 호출 할때 ServiceConnection을 제공해야 한다. bindService()
반환 값은 요청된 서비스가 존재하는지, 클라이언트에 서비스 액세스 권한이 있는지를 나타낸다.
onServiceConnected()
메서드에는 IBinder
인수가 포함되어 있어 클라이언트가 이를 통해 바인드된 서비스와 통신할 수 있다.
바인딩을 지원하는 서비스를 생성할 때 클라이언트가 서비스와 상호작용하는데 사용할 수 있는 이넡페이스를 제공하는 IBinder를 필요로 한다.
자체적인 Binder class를 구현하는 방식, 메신저를 사용하는 방식, AIDL을 사용하는 방식 등이 있다.
현재 구현하고자 하는 서비스가 앱 전용이고 Activity와 같은 프로세스에서 실행되는 경우이기 때문에 Binder 클래스를 확장하는 방식을 사용하게 되었다.
class LocationUploadForegroundService : LifecycleService(), LocationUploadService {
private val locationUploadServiceBinder = LocationUploadServiceBinder(this)
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
if (isStartForegroundService.not()) startForegroundService(intent)
return locationUploadServiceBinder
}
override fun stopService()
override fun setServiceEndTime(delayMillis: Long)
override fun getServiceEndTime(): Long?
}
interface LocationUploadService {
fun stopService()
fun setServiceEndTime(delayMillis: Long)
fun getServiceEndTime(): Long?
}
class LocationUploadServiceBinder(val service: LocationUploadService) : Binder()
Binder class를 확장한 LocationUploadServiceBinder를 구현했다.
모든 public 메서드와 property를 제공하지 않기 위해 LocationUploadService 구현 해서 제공했다.
class LocationUploadServiceConnection : ServiceConnection {
var locationUploadService: LocationUploadService? = null
private set
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
locationUploadService = (binder as? LocationUploadServiceBinder)?.service
}
override fun onServiceDisconnected(name: ComponentName?) {
locationUploadService = null
}
}
만약 Android 시스템이 클라이언트와 서비스 사이에 Connection을 생성하면 ServiceConnection
에서 onServiceConnected()
를 호출한다.
parameter로 받은 IBinder를 통해 서비스를 구할 수 있다.
이때 IBinder는 Service onBind
함수에서 반환값으로 받은 binder이다.
class PromiseDetailActivity : AppCompatActivity(), OnMapReadyCallback {
private val locationUploadServiceConnection = LocationUploadServiceConnection()
override fun onStart() {
super.onStart()
//...
bindService(intent, locationUploadServiceConnection, BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(locationUploadServiceConnection)
}
}
bind 이후 ServiceConnection을 통해 서비스의 public 함수를 사용할 수 있다.
bindService 이후 바로 ServiceConnection을 통해 서비스에 접근하지 못한다.
ServiceConnection onServiceConnected()
가 호출 되기전에 사용할려고 했기 때문이다.
위의 문제점으로 인해 bind와 ServiceConnection을 사용하는게 아니라 Broadcast Receiver와 Intent를 통해 서비스를 시작/종료를 구현 해 볼려고 했다.
//activity
override fun onCreate() {
//...
val intentFilter = IntentFilter().apply{
addAction(LocationUploadReceiver.ACTION_LOCATION_UPLOAD_SERVICE_START)
addAction(LocationUploadReceiver.ACTION_LOCATION_UPLOAD_SERVICE_STOP)
}
registerReceiver(locationUploadReceiver, intentFilter)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(locationUploadReceiver)
}
//현재 약속 상세화면에서 위치공유를 껏을때 동작
Intent(LocationUploadReceiver.ACTION_LOCATION_UPLOAD_SERVICE_STOP).apply {
putExtra(LocationUploadReceiver.PROMISE_ID_KEY, promiseUploadUiState.id)
}.let { intent ->
sendOrderedBroadcast(intent, null)
}
서비스 시작, 종료 action을 IntentFilter에 설정하고 registerReceiver()
에서 구현한 receiver와 함께 intentFilter를 매개변수로 넣어주었다.
동작에 따라 Intent에 action을 설정하고 필요한 값들을 넣어주었다.
//Broadcast Receiver 내부
override fun onReceive(context: Context?, intent: Intent?) {
when (intent.action) {
ACTION_LOCATION_UPLOAD_SERVICE_START -> {
val promiseDateTime = intent.getStringExtra(PROMISE_DATE_TIME_KEY)
startLocationUploadForegroundService(context, promiseId, promiseDateTime)
}
ACTION_LOCATION_UPLOAD_SERVICE_STOP -> stopLocationUploadForegroundService(conte xt, promiseId)
}
}
Broadcast Receiver에서 받은 intent action에 따라 서비스에 다시 action과 데이터들을 보내준다.
//Service 내부
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (uploadRequests.isEmpty()) {
startForeground(FOREGROUND_NOTIFICATION_ID, buildNotification())
}
if (intent != null) handleUploadRequest(intent)
return super.onStartCommand(intent, flags, startId)
}
받은 intent를 통해 서비스 로직을 실행한다.
- Intent를 통해 action과 데이터를 전달하기 때문에 ServiceConnection처럼 Service의 public 메서드로 동작하는것에 비해 좀 더 많은 작업이 있다.
- 새로운 기능을 추가할때마다 새로운 action과 이에 맞는 key 상수들을 만들어야한다.
위에서 receiver로 intent를 받아서 service로 startForegroundService(intent)
로 넘겨주었는데 broadcastReceiver가 아닌 activity에서 직접 service에 넘겨주는거와 차이가 없다.
본래 회의에서 수정, 삭제 fcm을 받으면 위치공유 서비스에 알리려고했지만, 상세 화면에 진입해야 위치공유를 컨트롤 할 수 있게 수정되었기 때문에 Broadcast Receiver를 사용할 필요없이, activity에서 직접 서비스에 intent를 넘겨서 처리하는것이 좋지 않을까 생각한다.
//activity
private fun sendPromiseUploadInfoToReceiver() {
val locationUploadIntent = Intent(this@PromiseDetailActivity, LocationUploadForegroundService::class.java)
//intent 설정
startForegroundService(locationUploadIntent)
}
현재 Service에서 약속 정보 수정에 따라 위치 공유 끝나는 시간을 바꾸고 있는데 만약 이 로직을 서비스에서 분리하게 되면 다시 Broadcast Receiver를 사용해보는것을 고려할 수 있을것같다.