바인드된 서비스(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()
}
서비스와 통신할 액티비티 만들기
서비스를 실행하고 게임 설치 화면을 보여줄 Activity
는 GameDetailActivity
이다. 위 서비스 클래스에서 InstallActivity
에 대한 내용이 있는데, InstallActivity
는 앱의 뒤로가기 스택 상태를 유지하기 위해서 추가로 만든 Activity이다. 유저가 보기에는 그냥 바로 GameDetailActivity
로 돌아가는 것 처럼 보인다.
InstallActivity
class InstallActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 일단은 UI 없이 감
onBackPressed()
}
}
GameDetailActivity
Activity
는 ServiceConnection
을 통해서 서비스와 통신을 할 수 있다. 예제 코드에서 보면 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()
}
}
바인드된 서비스에 onStartCommand
에 START_STICKY
를 반환해서 시작된 서비스와 결합하게 된다면 죽지않는 바인드된 서비스도 만들 수 있다. 하지만 예제에서 앱 다운로드 상태도 관찰하지 않을 거면서 굳이 서비스를 살려 둘 필요가 없으므로 적절한 때 바인드 해제를 해주는 식으로 해 주었다!
AndroidManifest에 추가
방금 만든 서비스 클래스를 AndroidManifest에 등록 해 주어야 제대로 동작한다. <application> 태그 안에 다음과 같이 적어 주자.
<service android:name=".InstallStateService"/>
'프로그래밍 > Android' 카테고리의 다른 글
[안드로이드] 에뮬레이터 감지 하는 법 (Detecting Emulator Device) (1) | 2021.03.08 |
---|---|
[안드로이드] RecyclerView를 잘 사용하기 위한 팁들. (0) | 2021.02.05 |
[안드로이드] 패키지명(Package name)으로 앱 실행하기 (0) | 2021.01.18 |
[안드로이드] Firebase Crashlytics 연동방법 (0) | 2021.01.15 |
[안드로이드] 프로젝트에 Sentry 연동방법, Proguard 적용까지 (0) | 2021.01.12 |