사용자 눈에 띄는 작업 수행.
음악플레이어 앱 - 현재 재생중인 노래 표시
피트니스 앱 - 달리기 이동거리 알림 표시
포그라운드 서비스는 알림을 표시하여 리소스를 소비하고 있음을 유저에게 알리며 서비스를 중지하거나 포그라운드에서 제거하지 않는 한 해제 불가. (안드로이드 13 이상부터 유저가 해제 가능)
WorkManager API를 사용하는 것이 좋음.
📝 사용자가 알림 해제
안드로이드 13 (API 레벨 33)부터 사용자가 스와이프 제스처를 통해 포그라운드 서비스와 연결된 알림 해제 가능.
사용자가 알림을 해제할 수 없도록 하려면 Notification.Builder를 사용하여 알림을 만들때
setOngoing() 메서드에 true 전달.
📝 알림을 즉시 표시하는 서비스
포그라운드 서비스에 아래 특성 중 하나 이상이 있는 경우 안드로이드 12 이상을 실행하는 기기에서도 서비스가 시작되는 즉시 관련 알림이 표시됨.
- 서비스가 액션 버튼이 포함된 알림과 연결되어 있음.
- 서비스의 포그라운드 서비스 타입이 mediaProjection, mediaPlayBack, phoneCall
- 서비스가 알림을 설정할때 setForegroundServiceBehavior()에 FOREGROUND_SERVICE_IMMEDIATE를 전달.
📝 Foreground 서비스 권한 요청
안드로이드 9 (API 레벨 28)이상을 대상으로 하고 포그라운드 서비스를 사용하는 앱일 경우
매니페스트 파일에 FOREGROUND_SERVICE 권한을 요청해야 함.
API 레벨 28 이상을 대상으로 하는 앱이 FOREGROUND_SERVICE 권한을 요청하지 않고 포그라운드 서비스를 만들려고 할 경우 SecurityException 발생
📝 포그라운드 서비스 시작
서비스 내부의 onStartCommand()에서 서비스를 포그라운드에서 실행하도록 요청
//status bar의 상태 표시줄 알림을 식별할 수 있는 양수인 정수와 알림 객체 필요
val pendingIntent: PendingIntent =
Intent(this, ExampleActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE)
}
val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build()
// Notification ID cannot be 0.
startForeground(ONGOING_NOTIFICATION_ID, notification)
📝 백그라운드 시작 제한
안드로이드 12 (API 레벨 31) 이상을 대상으로 하는 앱은 특별한 경우를 제외하고는 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 없음.
백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 있는 경우
만약 한 앱이 다른 앱이 소유한 포그라운드 서비스를 시작하기 위해 Context.startForegroundService() 를 호출하는 경우 두 앱이 모두 안드로이드 12 이상을 대상으로 하는 경우에만 위의 제한이 적용됨.
📝 포그라운드에서 서비스 제거
stopForeground() 호출. 서비스가 포그라운드에서 실행되는 동안 서비스를 중지할 경우 해당 알림이 제거됨.
사용자가 직접 인지하지 못하는 작업 수행.
예시
앱이 서비스를 사용하여 저장 공간을 압축하는 경우.
네트워크를 통해 데이터를 가져오는 경우
API 레벨 26 이상을 대상으로 하는 앱이 포그라운드에 있지 않을 때 시스템은 백그라운드 서비스 실행에 대해 제한한다. (예를 들어 대부분의 상황에서는 백그라운드에서 위치 정보에 액세스해서는 안됨.)
앱 컴포넌트가 bindService()를 호출하여 서비스에 바인딩.
다른 컴포넌트가 서비스와 상호 작용하고, 클라이언트 - 서버 인터페이스를 제공하여 프로세스 간 통신(IPC)을 가능하게 함.
바인딩된 서비스는 다른 앱 컴포넌트가 바인딩되어 있는 동안에만 실행되며,
여러 컴포넌트가 한번에 서비스에 바인딩될 수 있지만, 모든 컴포넌트가 바인딩 해제되면 서비스가 파괴됨.
📝 서비스에 바인딩
onBind() 콜백 메서드 구현.
onBind() 메서드는 클라이언트가 서비스와 상호 작용할 때 사용하는 인터페이스를 정의하는 IBinder 객체 반환.
일반적으로 onBind() 혹은 onStartCommand()중 하나를 구현.
두 가지 모두 구현하는 경우 - 음악 플레이어
사용자가 애플리케이션을 나가도 음악이 계속 재생되며, 사용자가 애플리케이션으로 돌아오면
액티비티가 서비스에 바인딩하여 재생을 다시 제어할 수 있게 됨.
📝 IBinder 정의 방법
class LocalService : Service() {
// Binder given to clients.
private val binder = LocalBinder()
// Random number generator.
private val mGenerator = Random()
/** Method for clients. */
val randomNumber: Int
get() = mGenerator.nextInt(100)
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
// Return this instance of LocalService so clients can call public methods.
fun getService(): LocalService = this@LocalService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
}
//onServiceConnected() 콜백을 사용하여 서비스에 바인딩
class BindingActivity : Activity() {
private lateinit var mService: LocalService
private var mBound: Boolean = false
/** Defines callbacks for service binding, passed to bindService(). */
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance.
val binder = service as LocalService.LocalBinder
mService = binder.getService()
mBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to LocalService.
Intent(this, LocalService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
/** 명시적 인텐트를 사용하여 앱 보안을 유지해야 함.
* 안드로이드 5 (API 레벨 21) 부터 암시적 인텐트로 bindService 호출 시 예외 발생 **/
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
mBound = false
}
/** Called when a button is clicked (the button in the layout file attaches to
* this method with the android:onClick attribute). */
fun onButtonClick(v: View) {
if (mBound) {
// Call a method from the LocalService.
// However, if this call is something that might hang, then put this request
// in a separate thread to avoid slowing down the activity performance.
val num: Int = mService.randomNumber
Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
}
}
}
/** Command to the service to display a message. */
private const val MSG_SAY_HELLO = 1
class MessengerService : Service() {
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
private lateinit var mMessenger: Messenger
/**
* Handler of incoming messages from clients.
*/
internal class IncomingHandler(
context: Context,
private val applicationContext: Context = context.applicationContext
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SAY_HELLO ->
Toast.makeText(applicationContext, "hello!", Toast.LENGTH_SHORT).show()
else -> super.handleMessage(msg)
}
}
}
/**
* Messenger는 안드로이드 메신저 클래스
*/
override fun onBind(intent: Intent): IBinder? {
Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
mMessenger = Messenger(IncomingHandler(this))
return mMessenger.binder
}
}
class ActivityMessenger : Activity() {
/** Messenger for communicating with the service. */
private var mService: Messenger? = null
/** Flag indicating whether we have called bind on the service. */
private var bound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mService = Messenger(service)
bound = true
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service has been
// unexpectedly disconnected—that is, its process crashed.
mService = null
bound = false
}
}
fun sayHello(v: View) {
if (!bound) return
// Create and send a message to the service, using a supported 'what' value.
val msg: Message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
try {
mService?.send(msg)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
}
override fun onStart() {
super.onStart()
// Bind to the service.
Intent(this, MessengerService::class.java).also { intent ->
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
// Unbind from the service.
if (bound) {
unbindService(mConnection)
bound = false
}
}
}
📝 바인딩 된 서비스 생명주기 관리
모든 클라이언트가 바인딩된 서비스를 해제하면 안드로이드 시스템이 서비스 삭제하며 따로 수명 주기를 관리하지 않아도 됨. (startService()를 사용하지 않았을 경우에만)