Android Foreground Service (포그라운드 서비스)

woga·2021년 12월 19일
2

Android 공부

목록 보기
13/49
post-thumbnail

서비스란?

  • 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 컴포넌트이다.
  • 사용자 인터페이스를 제공하지 않는다.
  • 다른 애플리케이션 컴포넌트가 서비스를 시작할 수 있으며, 이는 사용자가 다른 애플리케이션으로 전환하더라도 백그라운드에서 계속해서 실행된다.
  • 구성 요소를 서비스에 바인딩하여 서비스와 상호작용할 수 있으며, 심지어는 프로세스 간 통신(IPC)도 수행할 수 있다.
  • 별도로 지정하지 않는 한 서비스는 주로 메인 스레드에서 실행된다.

ex) 한 서비스는 네트워크 트랜잭션을 처리하고, 음악을 재생하고 파일 I/O를 수행하거나 콘텐츠 제공자와 상호작용할 수 있으며 이 모든 것을 백그라운드에서 수행할 수 있습니다.

서비스에는 3가지 유형이 있으며, 포그라운드 서비스, 백그라운드 서비스, 바인드 이 글에서는 포그라운드 서비스를 다룹니다.

그 중 포그라운드 서비스란?

Foreground services perform operations that are noticeable to the user.
포그라운드 서비스는 사용자에게 눈에 띄는 작업을 수행합니다.

ex) 오디오앱, 사용자의 달리기를 기록하는 피트니스 앱(물론 퍼미션 필요), 파일 다운로드 등등

위 사진처럼 사용자가 눈에 띄는 작업을 수행해야 하는 경우에만 알림을 표시해야 합니다. 포그라운드 서비스는 사용자가 앱과 상호 작용하지 않는 경우에도 계속 실행됩니다. 이는 서비스가 중지되거나 포그라운드에서 제거되지 않는 한 알림을 해제할 수 없음을 의미합니다.

또한, 포그라운드 서비스는 활성화된 액티비티와 동급의 우선순위를 가집니다. 그래서 메모리가 부족하더라도 안드로이드 시스템 의해 종료될 확률이 적습니다.

그럼 우린 왜 이 사용자에게 눈에 띄는 작업을 정의해서 포그라운드 서비스로 사용해야하는 걸까?


서비스는 백그라운드에서 실행되며, 백그라운드에서 위치, 카메라 등의 자원을 소비할 수 있습니다.

-> 참고로 앱의 포그라운드 서비스가 기기의 위치와 카메라에 액세스해야 하는 경우 매니페스트에 다음과 같이 서비스를 선언 <service ... android:foregroundServiceType="location|camera" />


UI가 없기 때문에 사용자는 앱에서 실행 중인 서비스 유형사용 중인 리소스를 인식하지 못합니다. 이는 보안과 성능 모두에 영향을 미칩니다.

그래서 이 때 포그라운드 서비스가 필요한 것입니다.


포그라운드 서비스는 앞서 말했다 싶이 사용자에게 눈에 띄는 작업, (실행되고 있어!! 잊지마!! 존재감을 펼치기) 입니다.
그렇다면 상태 표시줄에 알림을 표시해야하는데, 이 때 알림의 우선 순위는 PRIORITY_LOW 이상이어야 합니다.
알림은 작업이 완료되어 서비스가 자체적으로 또는 다양한 요인으로 인해 시스템에서 중지하거나 제거되지 않는 한 서비스를 중지 및 제거할 수 없습니다.

이렇게 하면 시스템 리소스를 소모할 수 있는 일부 작업이 백그라운드에서 수행되고 있음을 사용자가 알 수 있습니다.

그래서 포그라운드 서비스하면 간단한 예로 오디오 플레이어 앱을 꼽는 이유입니다.

앱이 라이브 상태가 아닐 때도 노래를 재생하는 동안 알림을 표시하기 때문에 서비스와 상호작용할 수 있으며, 현재 재생 중인 노래와 관련 정보들을 표시할 수 있습니다.
이 때, 알림은 서비스 종료 시에만 삭제됩니다.

그래서 주문 추적으로 보여주는 배달 앱, 우버와 같은 택시 타기 앱 (라이드가 진행 중인 경우) 등의 알림에서 라이드 트랙을 표시하는 등의 작업도 보여줄 수 있습니다.

어떻게 구현하는가?

서비스 구현하는 것과 비슷하지만 약간 다릅니다. 단계별로 어떻게 구현하는지 따라가보겠습니다

1 단계

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

2 단계

원하는 대로 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"

onStartCommand의 반환값

서비스는 언제든지 시스템에 의해 자원이 부족한 상황에 종료될 수 있습니다.
우리는 이러한 상황을 대비하기 위해 우아한(gracefully) 재시작 루틴을 설계해야 합니다.
이러한 루틴을 설계할 때 선택할 수 있는 옵션들은 onStartCommand의 반환값을 통해 결정됩니다. 어떤 것을 반환하느냐에 따라 재시작될 때 onStartCommand가 호출되는 방식이 달라집니다.


START_NOT_STICKY: 서비스를 명시적으로 다시 시작할 때 까지 만들지 않습니다.

START_STICKY: 서비스를 다시 만들지만 마지막 Intent를 onStartCommand의 인자로 다시 전달하지 않습니다. 이는 일단 계속 살아있어야되지만 별다른 동작이 필요하지 않은 음악앱같은 서비스에 적절합니다.

START_REDELIVER_INTENT: 이름에서 알겠듯이 마지막 Intent를 onStartCommand의 인자로 다시 전달해줍니다. 즉각적인 반응이 필요한 파일 다운로드 서비스 같은 곳에 적합합니다.

3 단계

요구 사항을 정의해보자면,

  • 시작 버튼을 클릭하면 서비스가 시작되고 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"
    }
}

cf) 서비스가 실행중인지는 어떻게 확인할까?

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
}

4 단계

그리고 현재 실행되고 있다는 것을 시스템의 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에서 표시된 상태를 보고 수동으로 중지할 수 있습니다.

요약

서비스는 여러 가지 방법으로 사용할 수 있지만, 포그라운드 서비스로 제한을 두는 것은 보안과 성능을 향상시킵니다.
요구 사항에 따라 포그라운드 서비스를 적절하게 사용하면 좋습니다.

기타, foreground는 정확하게 무엇인가?

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개 중에 하나라도 해당되면 포그라운드에 해당된다고 합니다.

  • 액티비티가 시작되었거나 일시 중지되었는지 여부에 관계없이 눈에 보이는 활동이 있다.
  • 포그라운드 서비스가 있다.
  • 다른 포그라운드 앱은 해당 서비스 중 하나에 바인딩하거나 content provider 중 하나를 사용하여 앱에 연결된다.

References

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://medium.com/mj-studio/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%95%84%EC%84%B8%EC%9A%94-2-1-service-foreground-service-e19cf74df390

https://betterprogramming.pub/what-is-foreground-service-in-android-3487d9719ab6

https://jizard.tistory.com/216

profile
와니와니와니와니 당근당근

0개의 댓글