본문 바로가기
카테고리 없음

Foreground Service 구현android.app.ForegroundServiceStartNotAllowedException 처리

by HlAos 2024. 8. 29.

Background Service : Android 6.0 (api level 23) 도입된 배터리 절약 기능 Doze 모드에 의해 시스템에 의해 Kill 당할 수 있다.

Foreground Service : 사용자가 인지할 수 있는 작업을 수행하며, 이 과정에서 반드시 알림을 통해 서비스가 실행 중임을 사용자에게 표시해야 합니다. 이러한 서비스는 사용자가 명시적으로 중지하지 않는 한 계속 실행되며, 중요한 작업이 중단되지 않도록 시스템에 의해 강제로 종료되지 않습니다.

Foreground Service는 상태 표시줄에 알림을 표시하며, 이 알림을 통해 사용자는 서비스가 활성화된 상태에서 시스템 리소스를 소비하고 있다는 것을 인지할 수 있습니다.

일반적인 Foreground Service 사용 사례로는 다음과 같은 예가 있습니다:

  • 음악 플레이어 앱: 포그라운드 서비스에서 음악을 재생하며, 알림에는 현재 재생 중인 곡 정보가 표시됩니다.
  • 피트니스 앱: 사용자의 운동을 기록하는 앱에서 포그라운드 서비스를 사용하여 사용자의 이동 기록을 저장하고, 권한 요청을 할 때 알림을 통해 진행 상황을 사용자에게 표시할 수 있습니다.

https://developer.android.com/develop/background-work/services/foreground-services?hl=ko

 

Foreground Service를 구현할 때, 간혹 android.app.ForegroundServiceStartNotAllowedException 오류가 발생할 수 있습니다.

 

Foreground Service는 startForegroundService()로 시작한 후, startForeground()를 호출해야 하며, startForegroundService() 호출 후 5초 이내에 startForeground()가 호출되지 않으면 시스템에 의해 서비스가 강제 종료되고 해당 오류가 발생합니다.

 

이 문제를 해결하기 위해 기존에 잘 사용하지 않던 bindService()를 사용하여 서비스를 바인딩하는 방식으로 구현했습니다.

bindService() 로 서비스 바인딩시 콜백 함수들의 실행 순서는 다음과 다음과 같다.

onCreate() -> onBind() -> onStartCommand()

 

1. 서비스의 생명주기

일반적으로 서비스 생성시 Service 를 상속받는 클래스를 만듭니다. 해당 서비스는 개발자가 직접 생명주기를 관리해야 합니다. 하지만 이방법은 액티비티와 서비스의 생명주기가 달라서 간혹 오류가 발생합니다.

class StepCounterService : Service()
변경
class StepCounterService : LifecycleService()

 

해당 문제 해결을 위해 LifecycleService() 사용하였습니다.

 

Android의 서비스 컴포넌트에 Lifecycle 기능을 추가한 클래스로, Android Jetpack의 Lifecycle-aware components(수명 주기 인식 컴포넌트)를 사용할 수 있게 해줍니다. 이 클래스는 Service의 하위 클래스이며, 서비스의 수명 주기를 Lifecycle 객체로 노출하여 옵저버들이 서비스의 수명 주기 변화를 인식할 수 있습니다.

  • 수명 주기 관리: LifecycleService는 서비스의 수명 주기 상태를 Lifecycle 클래스를 통해 관리하며, 이를 통해 수명 주기를 인식하는 컴포넌트(예: LiveData, ViewModel)를 서비스 내에서 쉽게 사용할 수 있습니다.
  • Observer 등록: LifecycleService를 통해 LifecycleObserver를 서비스에 등록하여 서비스의 생명 주기 상태 변화를 감지할 수 있습니다.

LifecycleService는 기본적으로 Service 클래스를 확장하므로, Service의 주요 메서드를 동일하게 사용할 수 있습니다. 추가적으로, getLifecycle() 메서드를 통해 Lifecycle 객체를 얻을 수 있습니다.

  • getLifecycle(): 서비스의 Lifecycle 객체를 반환합니다. 이 객체를 통해 LifecycleObserver를 등록하거나 수명 주기 상태를 확인할 수 있습니다.

다음은 LifecycleService를 사용하는 간단한 예시입니다.

class StepCounterService : LifecycleService() {

    private val observer = MyObserver()

    override fun onCreate() {
        super.onCreate()
        // LifecycleObserver 등록
        lifecycle.addObserver(observer)
    }

    override fun onDestroy() {
        super.onDestroy()
        // 서비스 종료 시 LifecycleObserver 해제
        lifecycle.removeObserver(observer)
    }
}

class StepObserver : LifecycleObserver {
    
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onServiceStart() {
        // 서비스가 시작될 때 수행할 작업
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onServiceStop() {
        // 서비스가 중지될 때 수행할 작업
    }
}
 

LifecycleService의 주요 장점

  1. 서비스에서 수명 주기 인식 컴포넌트 사용 가능: LifecycleService는 LiveData, ViewModel, LifecycleObserver와 같은 컴포넌트를 서비스 내에서 사용할 수 있게 해줍니다.
  2. 수명 주기 관리의 간소화: 수명 주기를 별도로 추적하고 관리하지 않아도, LifecycleObserver를 통해 상태 변화에 따른 처리가 가능해집니다.

 

2. onBind() 구현

  • 보통 onBind()를 구현하지 않는데 액티비티와 직접적으로 상호작용하지 않거나 제한적이기 때문에 굳이 구현할 필요가 없다.
  • 하지만 5초 이내에 startForeground() 시작하기 위하여 구현하였고 Activity or Fragment or Receiver 에서 bindService()를 통해 서비스를 바인딩하고 IBinder 인터페이스를 통해 클라이언트와 서비스간에 상호작용을 하도록 하였다.
  • Activity or Fragment or Receiver 직접적으로 서비스와 상호작용을 위해서는 onBind() 구현이 필수적이다.
  • 클라이언트가 서비스의 메서드를 호출할 수 있도록 localBinder 반환해 준다.
 override fun onBind(intent: Intent): IBinder {
   super.onBind(intent)
    
    StepServiceConnection.bindService()
    startStepServiceBind()
 }
 
 private fun startStepServiceBind() {
    val serviceIntent = Intent(this, StepCounterService::class.java)
    ContextCompat.startForegroundService(this, serviceIntent)
 }
    
  override fun onUnbind(intent: Intent?): Boolean {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            stopSelf()
    }
    stopForeground(true)
  }
  
  internal inner class LocalBinder: Binder() {
     fun getService(): StepCounterService = this@StepCounterService
  }

 

3. ServiceConnection 구현

object StepServiceConnection: ServiceConnection {
  	private var stepCounterService: StepCounterService? = null

    private var isBound = false
        private set

    // 서비스를 바인드합니다.
    fun bindService() {
        isBound = true
    }
    
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
    	val binder = service as StepCounterService.LocalBinder
        this.stepCounterService = binder.getService()
        bindService()
    }

  	override fun onServiceDisconnected(name: ComponentName?) {
        stepCounterService = null
        isBound = false
    }
}

 

4. onStartCommand() 구현

  • onStartCommand() 서비스가 startService() or startForegrundService() 로 명시적으로 시작시 호출됨
  • onBind() 에서 startForegroundService() 호출하기 때문에 onStartCommand()가 호출되고 startForeground() 통해 서비스가 5초안에 시작된다. 이렇게 ForegroundServiceStartNotAllowedException 를 방지하였다.
 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
   super.onStartCommand(intent, flags, startId)
        
   try {
    	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
       		ServiceCompat.startForeground(this, NOTIFICATION_CHANNEL_TYPE_STEP_COUNT, getNotification(this), FOREGROUND_SERVICE_TYPE_HEALTH)
    	} else {
        	ServiceCompat.startForeground(this, NOTIFICATION_CHANNEL_TYPE_STEP_COUNT, getNotification(this), 0)
    	}
   } catch (e: Exception) {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) {
           e.printStackTrace()
       }
   }
   
   return START_STICKY
 }