ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Android Foreground Service 예제
    안드로이드 학습/Android 기술면접 대비 코드 2023. 12. 22. 17:40

    1) 시작전 참고 자료  및 참고 사항

    1. Service와 Foreground Service의 전반적인 설명 : 링크

    2. 안드로이드 developer 사이트 설명 : 링크

    3. API 33, API 34 변동 사항 설명된 블로그 글 : 링크

     

     

    먼저 알아야 할 내용은 안드로이드 API가 업데이트 될때마다 notification(알림) 이나 Service의 추가 되는 방법이 조금씩 달라진다는 것이다.

     

    구체적으로 예시를 들자면 다른 블로그에 여러 Foreground Service에 Notification을 추가 시키는 방법이 나와있지만, API 33, Android 13 버전 이상에서는 AndroidManifest.xml에 permission을 선언해줘야 notification이 되기 때문에 이전 버전들 내용대로 똑같이 해도 잘 실행되지 않을 수도 있다는 것이다.

     

    이 글도 2023년 12월 기준이기 때문에 몇년 지난다면 아마도 똑같이 사용해도 Android Foreground 실행과 Notification 부분이 실행이 되지 않을 수도 있다. 

     

    이렇게 추가된 것들은 그때그때 찾아가면서 추가 시켜야 될 듯 싶다.

     

    전체 코드만 보고 싶으면 아래로 가세요.

     

    2) AndroidManifest.xml 파일

    더보기
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    
        <application
    
    		... 생략 ...
    
            <service
                android:name=".SimpleForegroundService"
                android:enabled="true"
                android:exported="true"
                android:foregroundServiceType="dataSync" />
        </application>
    
    </manifest>

     

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> // !!! 이부분 !!!

     

    우선 ForegroundService와 Notification 을 사용할 것임을 Manifest.xml에 추가시켜준다. API 33부터는 Notification 권한 부분도 반드시 추가 시켜줘야 한다고 한다. 

     

    이 부분을 추가 안시켜줘서 실행이 안되 한참을 헤맸다...

     

    android:exported="true"
    android:foregroundServiceType="dataSync"

     

    그 다음 Service를 application tag 내부에 추가 시켜준다. 

    • exported : 다른 앱에서도 접근 가능하게 할지 아닐지를 정해주는 것이라고 한다. 
    • foregroundServiceTypeAndroid :  Android 14, API 34부터는 어떤 타입으로 사용할지 반드시 추가해야 한다고 한다. 추가 되는 타입에 따라 요청되는 permission 선언도 추가되어야 하므로 아래 개발자 페이지 링크에서 확인해보면서 추가하는게 좋을 듯 싶다. 

    Service가 가지고 있는 속성 종류 및 설명 : 링크

    foregroundServiceTypeAndroid 설명 : 링크

     

    3) ForegroundService.kt

     

    우선 큰 흐름을 보자면 Service는 생명주기 그림대로 onCreate() > onStartCommand() > onDestory() 순서대로 호출된다.

     

     

    3-1) Service와 Activity와 통신

     

    외부에서 intent에 아래처럼 정보를 넣어주고 Service에 보내면 된다.

     

    처음 시작할때도 아래처럼 action과 필요 정보를 넣어줘서 보내주고

    중간에 Service에서 처리하려는 일이 있다면 아래처럼 action에 문자열을 넣어주고 Service에서 일이 수행되게 할 수 있다.

     

    API 26 이상과 그 미만에서의 startService 방법이 다른가보다... 근데 API 26은 도대체 언제인거야?? 

    fun startService(context: Context, title: String, content: String) {
        val intent = Intent(context, SimpleForegroundService::class.java)
        intent.action = "StartForeground"
        intent.putExtra(Noti_Title, title)
        intent.putExtra(Noti_Content, content)
        if (Build.VERSION.SDK_INT < 26) {
            context.startService(intent)
        } else {
            context.startForegroundService(intent)
        }
    }

     

     

     

    Service 내부에서는 Intent를 이용해 action위에 "StartForeground" 같이 문자열을 넣으서 보내주면 onStartCommand에 받아서 아래처럼 분기를 나눠서 처리해 주며 된다.

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    
     	... 생략 ...
        when (intent.action) {
        
         	... 생략 ...
            
           "StartForeground" -> {
                Log.d(TAG, "ForegroundService - StartForeground")
                ... 생략 ...
            }
            
        	"StartProgress" -> {
         		Log.d(TAG, "ForegroundService - StartProgress")
            	startProgress()
         	}
       }
    
        
        return START_STICKY
    }

     

    더보기

    ✔️ START_STICKY 

    • 서비스가 강제로 종료되었을 때 시스템이 자동으로 다시 시작하도록 하는 방식입니다. 
    • 서비스가 다른 추가적인 작업 없이도 계속 실행되어야 하는 경우에 주로 사용됩니다. 시스템에 의해 재시작되면 onStartCommand()가 호출되지만 Intent는 Null이 전달됩니다. 

    ✔️ START_NOT_STICKY 

    • 서비스가 강제로 종료되었을 때 시스템이 자동으로 다시 시작하지 않도록 하는 방식입니다. 
    • 이 방식은 서비스가 특정 작업을 완료한 후에는 더 이상 실행되지 않아도 되는 경우에 주로 사용됩니다. 
    • 시스템 자원 부족으로 서비스가 강제종료되어도 문제가 없는 경우에 사용됩니다. 

    ✔️ START_REDELIVER_INTENT

    • 서비스가 강제로 종료되었을 때 시스템이 자동으로 다시 시작하고, 마지막으로 전달된 Intent를 다시 전달해주는 방식입니다.
    • 서비스가 실행 중인 작업을 반드시 완료해야 하는 경우에 주로 사용됩니다.
    • 마지막으로 전달된 Intent가 다시 동일하게 전달되므로 취소된 작업을 이어서 수행할 수 있습니다. 하지만 그만큼 시스템 자원을 더 사용하기 때문에 필수적인 작업의 완료를 보장하고 싶을 때에만 사용해야 합니다.

    ✔️ 각 Flag 사용 예시

     

    START_STICKY 백그라운드 음악 재생 서비스: 백그라운드에서 계속 음악을 재생해야 합니다. 위치 정보 추적 서비스: 사용자의 위치 정보를 계속 추적해야 합니다.

     

    START_NOT_STICKY 파일 다운로드 서비스: 사용자가 요청한 파일을 다운로드한 후에는 서비스가 종료되어도 됩니다. 일회성 작업 수행 서비스: 일회성 작업 수행 후에는 서비스가 종료되어도 무관합니다.

     

    START_REDELIVER_INTENT 네트워크 요청 서비스: 서버 통신을 수행하는 경우, 네트워크 상태가 불안정하거나 실패했을 때에도 다시 시작해야 합니다. 이 때 마지막으로 전달한 Intent를 그대로 다시 전달받아 작업을 이어서 수행할 수 있도록 합니다. 긴 작업 수행 서비스: 작업 수행에 긴 시간이 소요되는 서비스에서는 해당 작업이 완료되지 않았을 경우에 작업을 다시 시작해야 합니다.

     

    ✔️ 각 Flag 두줄 요약

     

    START_STICKY 서비스가 지속적으로 실행되어야 할 경우에 적합합니다.

    시스템에 의해 재시작되지만 Intent는 NULL이 전달됩니다.

     

    START_NOT_STICKY 서비스가 작업을 완료한 후 실행되지 않아도 되는 경우에 적합합니다.

    시스템에 의해 재시작되지 않습니다.

     

    START_REDELIVER_INTENT 서비스가 실행 중인 작업을 반드시 완료해야 하는 경우에 적합합니다.

    시스템에 의해 재시작되며, 마지막으로 전달했던 Intent를 전달합니다.

     

    3-2) 알림 채널 생성

     

    메서드를 호출하여 알림 채널을 생성한다. 안드로이드 O 버전 이상에서는 Foreground Service를 사용하기 위해 알림 채널을 설정해야 한다.

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                CHANNEL_ID,
                "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply { description = "Foreground Service 예제" }
            manager = getSystemService(
                NotificationManager::class.java
            )
            manager.createNotificationChannel(serviceChannel)
        }
    }

     

     

     

    3-3) action 버튼 생성 부분

     

    알림에서 버튼 클릭시 실행 되는 action을 나타낸다.

     

    Activity로 이동 : PendingIntent.getActivity를 이용해서 Activity로 이동한다.

    val notificationIntent = Intent(this, MainActivity::class.java)
    val pendingIntent = PendingIntent.getActivity(
        this,
        0,
        notificationIntent,
        PendingIntent.FLAG_IMMUTABLE
    )

     

    Service로 이동 : PendingIntent.getService를 이용해서 Service로 이동한다.

    //  Notification 클릭시 SimpleForegroundService 이동. Progress Bar 작동 시작
    val startProgressIntent = Intent(this, SimpleForegroundService::class.java)
    startProgressIntent.action = ("StartProgress")
    val startProgressPendingIntent = PendingIntent.getService(
        this,
        0,
        startProgressIntent,
        PendingIntent.FLAG_MUTABLE
    )

     

     

    PendingIntent란???

    더보기

    PendingIntent

     

    일반적인 Intent 로 알림에서의 동작을 구현했다면 정상적으로 동작하지 않는다.  왜냐하면 다른 앱에서부터 내가 정의한 Intent 를 실행한다는 권한이 없기 때문이다.

     

    PendingIntent는 Intent를 가지고 있는 클래스로, 기본 목적은 다른 어플리케이션(다른 프로세스)의 권한을 허가하여 가지고 있는 Intent를 마치 본인 앱의 프로세스에서 실행하는 것처럼 사용하는 것입니다.

     

    3-4) 알림 생성

    notification = NotificationCompat.Builder(this, CHANNEL_ID).apply {
        setSmallIcon(R.drawable.coffee_icon)
        setContentTitle(inputTitle)
        setContentText(inputContent)
        setProgress(100, 0, false)
        setStyle(
            NotificationCompat.BigTextStyle()
                .bigText(inputContent)
        )
        addAction(R.drawable.start, "start progress", startProgressPendingIntent)
        addAction(R.drawable.cancel, "stop progress", stopProgressPendingIntent)
        addAction(R.drawable.cancel, "stop service", stopPendingIntent)
        setContentIntent(pendingIntent)
    }
    startForeground(1, notification.build())

     

    Foreground Service를 시작하고, 생성한 알림을 전달한다. 1은 알림의 고유 ID이며, 여기서는 notification을 사용합니다.

     

    4) MainActivity.kt

     

    MainActivity는 따로 볼내용은 많이 없고 Notification 부분만 보면 될듯 하다.

    먼저 Notificaiton 권한을 확인하고 없으면 권한을 허가 받고 있으면 바로 Service를 시작한다.

    fun checkNotificationPermission(): Boolean {
        return when (PackageManager.PERMISSION_GRANTED) {
            ContextCompat.checkSelfPermission(this, permission) -> {
                true
            }
            else -> {
                false
            }
        }
    }
    
    btnStartService.setOnClickListener {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (checkNotificationPermission()) {
                SimpleForegroundService.startService(
                    this,
                    "Foreground Service Title",
                    "Foreground Service Content"
                )
            } else {
                requestNotificationPermission.launch(permission)
    
            }
        }
    }
    requestNotificationPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    startForegroundService(serviceIntent)
                } else {
                    startService(serviceIntent)
                }
            } else {
                Toast.makeText(
                    applicationContext,
                    "Notification permission을 승낙해주세요..",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }

     

     

    해당 예제 같은 경우 Service에 보내는 모든 function을 Service 의 companion object 넣어줘서 MainActivity에 불러오는 방식을 사용했다. 

     

    다른 예제에서 그렇게 했기 때문에 이런 방식을 이용했지만 그냥 MainActivity에 하는 것이 좋은지 Service에 넣어서 가져오는게 좋은지는 잘 모르겠다....  좀더 공부가 필요할 듯 싶다.

     

    Github 주소 : 링크

     

    스크린샷 :

     

    1. MainActivity

    더보기
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            serviceIntent = Intent(this, SimpleForegroundService::class.java)
    
            btnStartService = findViewById(R.id.buttonStartService)
    
            btnStopService = findViewById(R.id.buttonStopService)
    
            startProgress = findViewById(R.id.startProgress)
    
            stopProgress = findViewById(R.id.stopProgress)
    
            checkRunningServiceBtn = findViewById(R.id.checkRunningServiceBtn)
    
            changeNotiBtn = findViewById(R.id.changeNotiBtn)
            titleET = findViewById(R.id.titleET)
            contentET = findViewById(R.id.contentET)
    
            btnStartService.setOnClickListener {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    if (checkNotificationPermission()) {
                        SimpleForegroundService.startService(
                            this,
                            "Foreground Service Title",
                            "Foreground Service Content"
                        )
                    } else {
                        requestNotificationPermission.launch(permission)
    
                    }
                }
            }
            btnStopService.setOnClickListener { SimpleForegroundService.stopService(this) }
    
            startProgress.setOnClickListener {
                if (isServiceRunning(this))
                    SimpleForegroundService.startProgress(this)
                else
                    Toast.makeText(this, "Foreground Service를 켜고 시도해주세요.", Toast.LENGTH_SHORT).show()
    
            }
    
            stopProgress.setOnClickListener {
                if (isServiceRunning(this))
                    SimpleForegroundService.stopProgress(this)
                else
                    Toast.makeText(this, "Foreground Service를 켜고 시도해주세요.", Toast.LENGTH_SHORT).show()
            }
    
            changeNotiBtn.setOnClickListener {
                if (isServiceRunning(this))
                    SimpleForegroundService.sendMessageToService(
                        this,
                        titleET.text.toString(),
                        contentET.text.toString()
                    )
                else
                    Toast.makeText(this, "Foreground Service가 켜져있지 않습니다.", Toast.LENGTH_SHORT).show()
            }
    
            checkRunningServiceBtn.setOnClickListener {
                if (isServiceRunning(this))
                    Toast.makeText(this, "Foreground Service가 켜져있습니다.", Toast.LENGTH_SHORT).show()
                else
                    Toast.makeText(this, "Foreground Service가 켜져있지 않습니다.", Toast.LENGTH_SHORT).show()
            }
    
            requestNotificationPermission =
                registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
                    if (isGranted) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                            startForegroundService(serviceIntent)
                        } else {
                            startService(serviceIntent)
                        }
                    } else {
                        Toast.makeText(
                            applicationContext,
                            "Notification permission을 승낙해주세요..",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
        }
    
    
        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        fun checkNotificationPermission(): Boolean {
            return when (PackageManager.PERMISSION_GRANTED) {
                ContextCompat.checkSelfPermission(this, permission) -> {
                    true
                }
                else -> {
                    false
                }
            }
        }
    
        private fun isServiceRunning(context: Context): Boolean {
            val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
            for (rsi in am.getRunningServices(Integer.MAX_VALUE)) {
                if (SimpleForegroundService::class.java.name == rsi.service.className) {
                    return true
                }
            }
            return false
        }
    
    
        companion object {
            @RequiresApi(Build.VERSION_CODES.TIRAMISU)
            val permission = Manifest.permission.POST_NOTIFICATIONS
        }
    }

     

    2. activity_main.xml

    더보기
    <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">
    
        <Button
            android:id="@+id/buttonStartService"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="60dp"
            android:layout_marginTop="120dp"
            android:layout_marginEnd="60dp"
            android:text="Start Service"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/buttonStopService"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="25dp"
            android:text="Stop Service"
            app:layout_constraintEnd_toEndOf="@+id/buttonStartService"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintStart_toStartOf="@+id/buttonStartService"
            app:layout_constraintTop_toBottomOf="@+id/buttonStartService"
            tools:ignore="MissingConstraints" />
    
    
        <Button
            android:id="@+id/startProgress"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="25dp"
            android:text="Start Progress"
            app:layout_constraintEnd_toEndOf="@+id/buttonStopService"
            app:layout_constraintStart_toStartOf="@+id/buttonStopService"
            app:layout_constraintTop_toBottomOf="@+id/buttonStopService" />
    
        <Button
            android:id="@+id/stopProgress"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="stop Progress"
            app:layout_constraintEnd_toEndOf="@+id/startProgress"
            app:layout_constraintStart_toStartOf="@+id/startProgress"
            app:layout_constraintTop_toBottomOf="@+id/startProgress" />
    
    
        <Button
            android:id="@+id/checkRunningServiceBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="Check Service Running"
            app:layout_constraintEnd_toEndOf="@+id/changeNotiBtn"
            app:layout_constraintStart_toStartOf="@+id/changeNotiBtn"
            app:layout_constraintTop_toBottomOf="@+id/stopProgress" />
    
        <EditText
            android:id="@+id/titleET"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="35dp"
            android:ems="10"
            android:hint="title"
            android:inputType="text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/checkRunningServiceBtn" />
    
        <EditText
            android:id="@+id/contentET"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:ems="10"
            android:hint="content"
            android:inputType="text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/titleET" />
    
        <Button
            android:id="@+id/changeNotiBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="Change Notification Content"
            app:layout_constraintEnd_toEndOf="@+id/buttonStopService"
            app:layout_constraintStart_toStartOf="@+id/buttonStopService"
            app:layout_constraintTop_toBottomOf="@+id/contentET" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    3. SimpleForegroundService.kt

    더보기
    class SimpleForegroundService : Service() {
    
    
        private var TAG = "SimpleForegroundService"
        private var binder: IBinder? = null        // bind 된 클라이언트와 소통하기 위한 인터페이스
    
        private lateinit var notification: NotificationCompat.Builder
        private lateinit var manager: NotificationManager
    
        private var progressThread: Thread? = null
        private var startFlag = true
        private var threadIsRunning = true // Thread가 여려개 켲지는 것을 막기 위해서 
    
    
        var startProgressPoint = 0
    
        override fun onCreate() {
            // 서비스가 생성 될 때
            super.onCreate()
            Log.d(TAG, "ForegroundService - onCreate")
    
    //        manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        }
    
        override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    
            Log.d(TAG, "ForegroundService - onStartCommand")
    
            if (intent != null) {
    
                when (intent.action) {
    
                    // startService()에 의해 서비스가 시작될 때
                    "StartForeground" -> {
                        Log.d(TAG, "ForegroundService - StartForeground")
    
                        val inputTitle = intent.getStringExtra(Noti_Title)
                        val inputContent = intent.getStringExtra(Noti_Content)
    
                        //안드로이드 sdk 26버전 이상에서는 알림창을 띄워야 Foreground
                        createNotificationChannel()
    
                        // Notification 클릭시 MainActivity 이동
                        val notificationIntent = Intent(this, MainActivity::class.java)
                        val pendingIntent = PendingIntent.getActivity(
                            this,
                            0,
                            notificationIntent,
                            PendingIntent.FLAG_IMMUTABLE
                        )
    
                        //  Notification 클릭시 SimpleForegroundService 이동. Progress Bar 작동 시작
                        val startProgressIntent = Intent(this, SimpleForegroundService::class.java)
                        startProgressIntent.action = ("StartProgress")
                        val startProgressPendingIntent = PendingIntent.getService(
                            this,
                            0,
                            startProgressIntent,
                            PendingIntent.FLAG_MUTABLE
                        )
    
                        // Progress 정지
                        val stopProgressIntent = Intent(this, SimpleForegroundService::class.java)
                        stopProgressIntent.action = ("StopProgress")
                        val stopProgressPendingIntent = PendingIntent.getService(
                            this,
                            0,
                            stopProgressIntent,
                            PendingIntent.FLAG_MUTABLE
                        )
    
    
                        // 버튼 클릭시 Foreground Service 종료
                        val stopServiceIntent = Intent(this, SimpleForegroundService::class.java)
                        stopServiceIntent.action = ("StopService")
                        val stopPendingIntent = PendingIntent.getService(
                            this,
                            0,
                            stopServiceIntent,
                            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
                        )
    
    
                        notification = NotificationCompat.Builder(this, CHANNEL_ID).apply {
                            setSmallIcon(R.drawable.coffee_icon)
                            setContentTitle(inputTitle)
                            setContentText(inputContent)
                            setProgress(100, 0, false)
                            setStyle(
                                NotificationCompat.BigTextStyle()
                                    .bigText(inputContent)
                            )
                            addAction(R.drawable.start, "start progress", startProgressPendingIntent)
                            addAction(R.drawable.cancel, "stop progress", stopProgressPendingIntent)
                            addAction(R.drawable.cancel, "stop service", stopPendingIntent)
                            setContentIntent(pendingIntent)
                        }
    
                        startForeground(1, notification.build())
    
                    }
    
                    // Progess 시작
                    "StartProgress" -> {
                        Log.d(TAG, "ForegroundService - StartProgress")
                        startProgress()
                    }
    
                    // Progress 정지
                    "StopProgress" -> {
                        Log.d(TAG, "ForegroundService - StopProgress")
                        stopProgress()
                    }
    
                    "StopService" -> {
                        Log.d(TAG, "ForegroundService - stopSelf")
    
                        // 서비스를 정지시키는 로직을 구현합니다.
                        stopForeground(STOP_FOREGROUND_REMOVE)
                        stopSelf()
                    }
    
                    // Notification 제목과 내용 바꿀때
                    "getMessageFromActivity" -> {
                        val inputTitle = intent.getStringExtra(Noti_Title)
                        val inputContent = intent.getStringExtra(Noti_Content)
                        notification.setContentTitle(inputTitle)
                        notification.setContentText(inputContent)
                        notification.setStyle(NotificationCompat.BigTextStyle().bigText(inputContent))
                        manager.notify(1, notification.build())
                    }
    
                }
            }
    
            return START_STICKY
        }
    
        private fun startProgress() {
    
            progressThread = Thread {
                for (incr in startProgressPoint..100 step 5) {
                    if (startFlag) {
                        notification.setProgress(100, incr, false)
                        startProgressPoint = incr
                        manager.notify(1, notification.build())
                        try {
                            Thread.sleep(1 * 500.toLong())
                        } catch (e: InterruptedException) {
                            Log.d("TAG", "sleep failure")
                        }
                    } else {
                        startFlag = true
                        break
                    }
                }
                // When the loop is finished, updates the notification
                notification.setContentText("Download completed")
                manager.notify(1, notification.build())
            }
            if (threadIsRunning) { // Thread가 한번 켜지면 더 실행되지 않게 하기 위해서
                progressThread!!.start()
                threadIsRunning = false
            }
    
        }
    
        private fun stopProgress() {
            if (startFlag && progressThread != null) {
                startFlag = false
                threadIsRunning = true // Thread가 정지 되면 다시 Thread가 실행되도 되게
                progressThread!!.interrupt()
                progressThread = null
            }
        }
    
        private fun createNotificationChannel() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val serviceChannel = NotificationChannel(
                    CHANNEL_ID,
                    "Foreground Service Channel",
                    NotificationManager.IMPORTANCE_DEFAULT
                ).apply { description = "Foreground Service 예제" }
                manager = getSystemService(
                    NotificationManager::class.java
                )
                manager.createNotificationChannel(serviceChannel)
            }
        }
    
        companion object {
            const val CHANNEL_ID = "ForegroundServiceChannel"
    
            private const val Noti_Title = "Noti_Title"
            private const val Noti_Content = "Noti_Content"
            fun startService(context: Context, title: String, content: String) {
                val intent = Intent(context, SimpleForegroundService::class.java)
                intent.action = "StartForeground"
                intent.putExtra(Noti_Title, title)
                intent.putExtra(Noti_Content, content)
                if (Build.VERSION.SDK_INT < 26) {
                    context.startService(intent)
                } else {
                    context.startForegroundService(intent)
                }
            }
    
            fun sendMessageToService(context: Context, title: String, content: String) {
                val intent = Intent(context, SimpleForegroundService::class.java)
                intent.action = "getMessageFromActivity"
                intent.putExtra(Noti_Title, title)
                intent.putExtra(Noti_Content, content)
                if (Build.VERSION.SDK_INT < 26) {
                    context.startService(intent)
                } else {
                    context.startForegroundService(intent)
                }
            }
    
            fun stopService(context: Context) {
                val intent = Intent(context, SimpleForegroundService::class.java)
                context.stopService(intent)
            }
    
    
            fun startProgress(context: Context) {
                val intent = Intent(context, SimpleForegroundService::class.java)
                intent.action = "StartProgress"
                if (Build.VERSION.SDK_INT < 26) {
                    context.startService(intent)
                } else {
                    context.startForegroundService(intent)
                }
            }
    
            fun stopProgress(context: Context) {
                val intent = Intent(context, SimpleForegroundService::class.java)
                intent.action = "StopProgress"
                if (Build.VERSION.SDK_INT < 26) {
                    context.startService(intent)
                } else {
                    context.startForegroundService(intent)
                }
            }
    
        }
    
    
        override fun onBind(intent: Intent): IBinder? {
            // bindService()에 의해 서비스가 시작될 때
            Log.d("ForegroundService", "ForegroundService - onBind")
            return binder
        }
    
        override fun onDestroy() {
            // 서비스가 종료할 때
            Log.d("ForegroundService", "ForegroundService - onDestroy")
    
        }
    
    }

    AndroidManifest.xml

    더보기
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    
        <application
            android:allowBackup="true"
            android:dataExtractionRules="@xml/data_extraction_rules"
            android:fullBackupContent="@xml/backup_rules"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.ForegroundService"
            tools:targetApi="31">
            <activity
                android:name=".MainActivity"
                android:exported="true">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <service
                android:name=".SimpleForegroundService"
                android:exported="true"
                android:foregroundServiceType="dataSync" />
        </application>
    
    </manifest>
Designed by Tistory.