Skip to content

Activity와 Service 상호작용하기

양성현 edited this page Dec 13, 2022 · 1 revision

약속 상세 화면에서 위치 공유를 ON/OFF를 할 수 있다. 위치공유는 포그라운드 서비스로 동작하기 때문에 activity와 service가 상호작용하는방법에 대해 생각해봐야했다.

1. 시작된 서비스에 바인딩하기

서비스를 시작 후 bindService()를 통해 클라이언트가 서비스에 바인딩 되도록 할 수 있다.

서비스가 시작되고 바인드되면 모든 클라이언트가 바인딩을 해제해도 서비스가 소멸되지 않는다.

ServiceConnection

클라이언트는 ServiceConnection을 통해 서비스의 상태를 모니터링 할 수 있다.

bindService()을 호출 할때 ServiceConnection을 제공해야 한다. bindService()반환 값은 요청된 서비스가 존재하는지, 클라이언트에 서비스 액세스 권한이 있는지를 나타낸다.

onServiceConnected() 메서드에는 IBinder 인수가 포함되어 있어 클라이언트가 이를 통해 바인드된 서비스와 통신할 수 있다.

바인딩을 지원하는 서비스를 생성할 때 클라이언트가 서비스와 상호작용하는데 사용할 수 있는 이넡페이스를 제공하는 IBinder를 필요로 한다.

자체적인 Binder class를 구현하는 방식, 메신저를 사용하는 방식, AIDL을 사용하는 방식 등이 있다.

Service Connection으로 Activity와 상호작용하기

현재 구현하고자 하는 서비스가 앱 전용이고 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()가 호출 되기전에 사용할려고 했기 때문이다.

2. Broadcast Receiver와 Intent

위의 문제점으로 인해 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 상수들을 만들어야한다.

3. Broadcast Receiver가 필요할까? Intent로만 하기

위에서 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를 사용해보는것을 고려할 수 있을것같다.

Clone this wiki locally