ex) 한 서비스는 네트워크 트랜잭션을 처리하고, 음악을 재생하고 파일 I/O를 수행하거나 콘텐츠 제공자와 상호작용할 수 있으며 이 모든 것을 백그라운드에서 수행할 수 있습니다.
서비스에는 3가지 유형이 있으며, 포그라운드 서비스
, 백그라운드 서비스
, 바인드
이 글에서는 포그라운드 서비스를 다룹니다.
Foreground services perform operations that are noticeable to the user.
포그라운드 서비스는 사용자에게 눈에 띄는 작업을 수행합니다.
ex) 오디오앱, 사용자의 달리기를 기록하는 피트니스 앱(물론 퍼미션 필요), 파일 다운로드 등등
위 사진처럼 사용자가 눈에 띄는 작업을 수행해야 하는 경우에만 알림을 표시해야 합니다. 포그라운드 서비스는 사용자가 앱과 상호 작용하지 않는 경우에도 계속 실행됩니다. 이는 서비스가 중지되거나 포그라운드에서 제거되지 않는 한 알림을 해제할 수 없음을 의미합니다.
또한, 포그라운드 서비스는 활성화된 액티비티와 동급의 우선순위를 가집니다. 그래서 메모리가 부족하더라도 안드로이드 시스템 의해 종료될 확률이 적습니다.
서비스는 백그라운드에서 실행되며, 백그라운드에서 위치, 카메라 등의 자원을 소비할 수 있습니다.
-> 참고로 앱의 포그라운드 서비스가 기기의 위치와 카메라에 액세스해야 하는 경우 매니페스트에 다음과 같이 서비스를 선언 <service ... android:foregroundServiceType="location|camera" />
UI가 없기 때문에 사용자는 앱에서 실행 중인 서비스 유형과 사용 중인 리소스를 인식하지 못합니다. 이는 보안과 성능 모두에 영향을 미칩니다.
그래서 이 때 포그라운드 서비스가 필요한 것입니다.
포그라운드 서비스는 앞서 말했다 싶이 사용자에게 눈에 띄는 작업, (실행되고 있어!! 잊지마!! 존재감을 펼치기) 입니다.
그렇다면 상태 표시줄에 알림을 표시해야하는데, 이 때 알림의 우선 순위는 PRIORITY_LOW
이상이어야 합니다.
알림은 작업이 완료되어 서비스가 자체적으로 또는 다양한 요인으로 인해 시스템에서 중지하거나 제거되지 않는 한 서비스를 중지 및 제거할 수 없습니다.
이렇게 하면 시스템 리소스를 소모할 수 있는 일부 작업이 백그라운드에서 수행되고 있음을 사용자가 알 수 있습니다.
그래서 포그라운드 서비스하면 간단한 예로 오디오 플레이어 앱을 꼽는 이유입니다.
앱이 라이브 상태가 아닐 때도 노래를 재생하는 동안 알림을 표시하기 때문에 서비스와 상호작용할 수 있으며, 현재 재생 중인 노래와 관련 정보들을 표시할 수 있습니다.
이 때, 알림은 서비스 종료 시에만 삭제됩니다.
그래서 주문 추적으로 보여주는 배달 앱, 우버와 같은 택시 타기 앱 (라이드가 진행 중인 경우) 등의 알림에서 라이드 트랙을 표시하는 등의 작업도 보여줄 수 있습니다.
서비스 구현하는 것과 비슷하지만 약간 다릅니다. 단계별로 어떻게 구현하는지 따라가보겠습니다
Android 9(API 레벨 28) 이상을 대상으로 하는 앱에 대해 포그라운드 서비스 권하는 요청해야합니다.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
매니페스트에 적어주면, 시스템은 앱에 자동으로 권한을 부여하기 때문에 추가 처리가 필요하지 않습니다.
참고: 이 권한이 없으면 런타임 시 앱이 죽습니다 SecurityException
java.lang.SecurityException: Permission Denial: startForeground from pid=xxx, uid=xxxx requires android.permission.FOREGROUND_SERVICE
원하는 대로 xml을 만들고, 간단한 Service 클래스를 만들어봅시다.
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_service_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
android:layout_margin="50dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:layout_margin="50dp"
tools:ignore="MissingConstraints"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
import android.app.*
import android.content.Intent
import android.os.IBinder
import com.sample.sampleforegroundservice.MainActivity.Companion.ACTION_STOP_FOREGROUND
class SampleForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action != null && intent.action.equals(
ACTION_STOP, ignoreCase = true)) {
stopSelf()
}
return START_NOT_STICKY
}
}
여기서 ACTION_STOP
은 할당된 상수입니다.
const val ACTION_STOP = "${BuildConfig. APPLICATION_ID }.stop"
서비스는 언제든지 시스템에 의해 자원이 부족한 상황에 종료될 수 있습니다.
우리는 이러한 상황을 대비하기 위해 우아한(gracefully) 재시작 루틴을 설계해야 합니다.
이러한 루틴을 설계할 때 선택할 수 있는 옵션들은 onStartCommand
의 반환값을 통해 결정됩니다. 어떤 것을 반환하느냐에 따라 재시작될 때 onStartCommand
가 호출되는 방식이 달라집니다.
START_NOT_STICKY
: 서비스를 명시적으로 다시 시작할 때 까지 만들지 않습니다.
START_STICKY
: 서비스를 다시 만들지만 마지막 Intent를 onStartCommand의 인자로 다시 전달하지 않습니다. 이는 일단 계속 살아있어야되지만 별다른 동작이 필요하지 않은 음악앱같은 서비스에 적절합니다.
START_REDELIVER_INTENT
: 이름에서 알겠듯이 마지막 Intent를 onStartCommand의 인자로 다시 전달해줍니다. 즉각적인 반응이 필요한 파일 다운로드 서비스 같은 곳에 적합합니다.
요구 사항을 정의해보자면,
TextView
에서 상태가 업데이트TextView
에서 상태가 업데이트onCreate
TextView
에서 서비스 상태 업데이트import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.view.View
import android.widget.TextView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.btn_start)?.setOnClickListener {
startService(Intent(this, SampleForegroundService::class.java))
updateTextStatus()
}
findViewById<View>(R.id.btn_stop)?.setOnClickListener {
val intentStop = Intent(this, SampleForegroundService::class.java)
intentStop.action = ACTION_STOP
startService(intentStop)
Handler().postDelayed({
updateTextStatus()
},100)
}
updateTextStatus()
}
private fun updateTextStatus() {
if(isMyServiceRunning(SampleForegroundService::class.java)){
findViewById<TextView>(R.id.txt_service_status)?.text = "Service is Running"
}else{
findViewById<TextView>(R.id.txt_service_status)?.text = "Service is NOT Running"
}
}
private fun isMyServiceRunning(serviceClass: Class<*>): Boolean {
try {
val manager =
getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(
Int.MAX_VALUE
)) {
if (serviceClass.name == service.service.className) {
return true
}
}
} catch (e: Exception) {
return false
}
return false
}
companion object{
const val ACTION_STOP = "${BuildConfig.APPLICATION_ID}.stop"
}
}
private fun isMyServiceRunning(serviceClass: Class<*>): Boolean {
try {
val manager =
getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(
Int.MAX_VALUE
)) {
if (serviceClass.name == service.service.className) {
return true
}
}
} catch (e: Exception) {
return false
}
return false
}
그리고 현재 실행되고 있다는 것을 시스템의 status bar에 표시를 해야하기 때문에, notifiaction을 추가로 구현합니다.
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.sample.sampleforegroundservice.MainActivity.Companion.ACTION_STOP_FOREGROUND
class SampleForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action != null && intent.action.equals(
ACTION_STOP_FOREGROUND, ignoreCase = true)) {
stopForeground(true)
stopSelf()
}
generateForegroundNotification()
return START_STICKY
}
//Notififcation for ON-going
private var iconNotification: Bitmap? = null
private var notification: Notification? = null
var mNotificationManager: NotificationManager? = null
private val mNotificationId = 123
private fun generateForegroundNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intentMainLanding = Intent(this, MainActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(this, 0, intentMainLanding, 0)
iconNotification = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
if (mNotificationManager == null) {
mNotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
assert(mNotificationManager != null)
mNotificationManager?.createNotificationChannelGroup(
NotificationChannelGroup("chats_group", "Chats")
)
val notificationChannel =
NotificationChannel("service_channel", "Service Notifications",
NotificationManager.IMPORTANCE_MIN)
notificationChannel.enableLights(false)
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET
mNotificationManager?.createNotificationChannel(notificationChannel)
}
val builder = NotificationCompat.Builder(this, "service_channel")
builder.setContentTitle(StringBuilder(resources.getString(R.string.app_name)).append(" service is running").toString())
.setTicker(StringBuilder(resources.getString(R.string.app_name)).append("service is running").toString())
.setContentText("Touch to open") // , swipe down for more options.
.setSmallIcon(R.drawable.ic_alaram)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setWhen(0)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.setOngoing(true)
if (iconNotification != null) {
builder.setLargeIcon(Bitmap.createScaledBitmap(iconNotification!!, 128, 128, false))
}
builder.color = resources.getColor(R.color.purple_200)
notification = builder.build()
startForeground(mNotificationId, notification)
}
}
}
포그라운드를 시작하려면 startForeground()
를 호출하고, 중지하려면 stopForeground()
를 호출합니다.
보통 서비스가 한 번 시작되면 앱이 종료되고 다시 시작되더라고 중지되지 않습니다. 그래서 Textview
에서 표시된 상태를 보고 수동으로 중지할 수 있습니다.
서비스는 여러 가지 방법으로 사용할 수 있지만, 포그라운드 서비스로 제한을 두는 것은 보안과 성능을 향상시킵니다.
요구 사항에 따라 포그라운드 서비스를 적절하게 사용하면 좋습니다.
The system distinguishes between foreground and background apps. An app is considered to be in the foreground if any of the following is true:
- It has a visible activity, whether the activity is started or paused.
- It has a foreground service.
- Another foreground app is connected to the app, either by binding to one of its services or by making use of one of its content providers.
If none of those conditions is true, the app is considered to be in the background.
스택오버플로우 : What exactly is the foreground in Android?에 대한 답변 중
정확하게 안드로이드에서 foreground가 뭔지 묻는 질문에 아래에 3개 중에 하나라도 해당되면 포그라운드에 해당된다고 합니다.
https://developer.android.com/guide/components/services?hl=ko
https://developer.android.com/guide/components/foreground-services
https://medium.com/@jayd1992/foreground-services-in-android-e131a863a33d
https://betterprogramming.pub/what-is-foreground-service-in-android-3487d9719ab6