프로그래밍/Android

[안드로이드] 예제로 알아보는 바인드된 서비스 (Bound Service)

Lou Park 2021. 1. 18. 11:48

바인드된 서비스(Bound Service)는 서비스와 다른 안드로이드 컴포넌트들 (Activity, Fragment, Service)간에 서버와 클라이언트같은 관계를 구현할 수 있어 IPC(프로세스간 통신)를 가능하게 한다.

 

이번에 이를 이용해서 구현할 예제는 앱의 설치상황을 관찰하는 서비스이다. 게임 다운로드 버튼을 누르면, 구글 플레이가 실행되고 구글플레이 설치 진행 상황에 따라 버튼 상태가 바뀐다. Bind Service의 작동원리와 개념은 코드와 함께 보자!

 

 

이번에 만들어 볼 예제

 

 

서비스 만들기

설치 상태를 관찰 할 서비스는 InstallStateService이다. 먼저 Binder를 하나 만들어 주어야하는데, binder를 통해서 다른 앱 컴포넌트에서 이 서비스에 접근 할 수 있게 된다.


class InstallStateService : Service() {  
    private val binder = LocalBinder()  

    inner class LocalBinder : Binder() {  
        fun getService() = this@InstallStateService  
    }  
}

 

추가로 게임 상태를 관찰하기 위한 것들을 적어주자!

// 대상 게임의 packageName
var gamePackageName: String? = null
// 설치 상황
var installState: MutableLiveData<InstallState> = MutableLiveData(InstallState.IDLE)
enum class InstallState {
    IDLE, INSTALLING, DONE
}

companion object {
    const val INTENT_GAME_PACKAGE_NAME = "gamePackageName"
}

 

 

바인딩 된 서비스를 구현하려면, onBind()에서 방금 만든 바인더를 리턴 해 주어야한다. 시작된 서비스와 혼용해서 사용할 것이 아니라면 onStartCommand()에서 처리 해줄게 없다. onBind는 서비스가 바인딩되었을때 호출이 되는데, 이때 Intent를 이용해 정보를 넘겨줄 수도 있다.

예제에서는 설치할 대상 게임에 대한 패키지명을 넘겨주고 있다. 설치 상황은 설치하고 있는 상황이 아니니 InstallState.IDLE로 상태를 바꿔준다. 바인딩 된 서비스는 모든 컴포넌트와의 바인딩이 해제되면 죽는다.

override fun onBind(intent: Intent?): IBinder? {
    gamePackageName = intent?.extras?.getString(INTENT_GAME_PACKAGE_NAME)
    installState.postValue(InstallState.IDLE)
    return binder
}

오른쪽이 바인드 된 서비스의 생명주기

 

앱 다운로드 상태 관찰하기

앱 다운로드 상태를 관찰하기 위한 코드다.

 

바인드된 서비스가 어떻게 동작하는지 살펴보기 위한 예제이므로 이 부분은 이해하지 않아도된다! 그래도 코드를 살펴보면, onActiveChanged에서 설치가 활성화되면, 해당 세션의 정보 sessionInfo를 가지고 와서 그 패키지명이 내가 바인드 할 당시 넘겨준 패키지명과 일치하는지 확인하고, 만약에 일치한다면 InstallState.INSTALLING 설치중으로 업데이트하고 앱 내의 InstallActivity를 호출해서 앱으로 돌아간다.

private var sessionCallback = object: PackageInstaller.SessionCallback() {
    override fun onCreated(sessionId: Int) {}

    override fun onBadgingChanged(sessionId: Int) {}

    override fun onActiveChanged(sessionId: Int, active: Boolean) {
        if (active) {
            val sessionInfo = packageManager.packageInstaller.getSessionInfo(sessionId)
            if (sessionInfo?.appPackageName == gamePackageName) {
                installState.postValue(InstallState.INSTALLING)
                val launchIntent = Intent(applicationContext, InstallActivity::class.java)
                launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                startActivity(launchIntent)
            }
        }
    }

    override fun onProgressChanged(sessionId: Int, progress: Float) {
        // while installing
        val sessionInfo = packageManager.packageInstaller.getSessionInfo(sessionId)
        if (sessionInfo?.appPackageName == gamePackageName) {
            if (installState.value != InstallState.INSTALLING) {
                installState.postValue(InstallState.INSTALLING)
            }
        }
    }

    override fun onFinished(sessionId: Int, success: Boolean) {
        // Download completed, packageName은 받아올 수 없음
        installState.postValue(InstallState.DONE)
    }
}

방금 만든 sessionCallback을 서비스 생명 주기에따라 register하고 unregister해 준다. 이로서 서비스 클래스 완성이 끝났다!

override fun onCreate() {
    super.onCreate()
    // 2개 앱 동시에 받을때는 현재 활성화 된것밖에 ProgressChange안됨, 뒤에 다운받은거는 onCreated도 안됨
    packageManager.packageInstaller.registerSessionCallback(sessionCallback)
}

override fun onDestroy() {
    packageManager.packageInstaller.unregisterSessionCallback(sessionCallback)
    super.onDestroy()
}

 

 

서비스와 통신할 액티비티 만들기

서비스를 실행하고 게임 설치 화면을 보여줄 ActivityGameDetailActivity이다. 위 서비스 클래스에서 InstallActivity에 대한 내용이 있는데, InstallActivity는 앱의 뒤로가기 스택 상태를 유지하기 위해서 추가로 만든 Activity이다. 유저가 보기에는 그냥 바로 GameDetailActivity로 돌아가는 것 처럼 보인다.

 

 

InstallActivity

class InstallActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 일단은 UI 없이 감
        onBackPressed()
    }
}

 

GameDetailActivity

ActivityServiceConnection을 통해서 서비스와 통신을 할 수 있다. 예제 코드에서 보면 onServiceConnected에서 서비스의 바인더를 받아와 서비스에 접근 할 수 있다. 그 후로는 서비스 내의 MutableLiveData의 상태변화를 관찰하며 UI를 업데이트하고 있다. 액티비티 시작시에 Service를 바인드하고, 액티비티가 onDestroy상태로 돌아갈때 unbind해주면 이 서비스는 GameDetailActivity의 생명주기에 따라 생성되고 사라진다.

 

class GameDetailActivity : AppCompatActivity() {

    private val installConnection = object: ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as InstallStateService.LocalBinder
            // 앱 설치 상황을 업데이트 합니다.
            val installService = binder.getService()
            installService.installState.observe(this@GameDetailActivity, Observer {
                when (it) {
                    InstallStateService.InstallState.IDLE -> {
                        // 아무것도 하지 않은 상태!
                        pb_installing.visibility = View.GONE
                        tv_play_game.visibility = View.VISIBLE
                    }
                    InstallStateService.InstallState.DONE -> {
                        // 다운로드가 완료된 상태!
                        pb_installing.visibility = View.GONE
                        tv_play_game.visibility = View.VISIBLE
                        btn_header_play.setText(
                            if (PackageUtil.isInstalled(activity, game?.packageName)) {
                                R.string.const_play
                            } else {
                                R.string.const_download
                            }
                        )
                        tv_play_game.setText(
                            if (PackageUtil.isInstalled(activity, game?.packageName)) {
                                R.string.action_play_game
                            } else {
                                R.string.download
                            }
                        )
                    }
                    InstallStateService.InstallState.INSTALLING -> {
                        // 다운로드 중인 상태
                        pb_installing.visibility = View.VISIBLE
                        tv_play_game.visibility = View.INVISIBLE
                    }
                }
            })
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            // 서비스가 연결종료 되었을때 할 행동...
        }
    }

    override fun onStart() {
        super.onStart()
        // 서비스 바인드 시작
        val intent = Intent(this, InstallStateService::class.java).apply {
            putExtra(InstallStateService.INTENT_GAME_PACKAGE_NAME, game?.packageName)
        }
        bindService(intent, installConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        // 서비스 바인드 해제
        unbindService(installConnection)
        super.onDestroy()
    }

}

바인드된 서비스에 onStartCommandSTART_STICKY를 반환해서 시작된 서비스와 결합하게 된다면 죽지않는 바인드된 서비스도 만들 수 있다. 하지만 예제에서 앱 다운로드 상태도 관찰하지 않을 거면서 굳이 서비스를 살려 둘 필요가 없으므로 적절한 때 바인드 해제를 해주는 식으로 해 주었다!

 

AndroidManifest에 추가

방금 만든 서비스 클래스를 AndroidManifest에 등록 해 주어야 제대로 동작한다. <application> 태그 안에 다음과 같이 적어 주자.

<service android:name=".InstallStateService"/>