본문 바로가기
공부/Android Studio

[Android/Kotlin] Foreground Service 사용 방법

by 웅대 2022. 12. 24.
728x90
반응형

음악을 틀어놓고 웹 서핑을 한다거나 애플리케이션을 설치하면서 문자를 한다거나 이렇게 특정 동작을 수행 도중에 다른 동작도 수행할 일이 있을 수 있다.

 

위의 예시에서 음악, 앱 설치의 경우 눈에 보이지 않는 일종의 백그라운드 서비스로 실행이 된다.

 

안드로이드에서는 백그라운드 서비스를 실행할 때는 반드시 notification을 띄워서 사용자에게 백그라운드로 실행 중인 앱이 있다고 알려줘야 한다.

 

만약 사용자가 백그라운드 서비스를 실행 중인 것을 모르고 계속  사용한다면 배터리나 성능이 저하될 수 있기 때문이다.

 

이렇게 백그라운드 서비스가 실행중일 때 notification을 띄우는 것을 포어그라운드(foreground) 서비스라고 한다.

 

포어그라운드 서비스에 대해서 공부하기 위해 1부터 100까지 숫자를 증가하는 앱을 백그라운드로 실행시키고 notification을 띄우는데 notification을 클릭하면 해당 액티비티로 이동하는 앱을 만들어보려 한다.

 

<메인 액티비티 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">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="실행"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

ForeService 클래스를 Service()를 상속받아 만든다.

class ForeService : Service(){
    override fun onBind(p0: Intent?): IBinder? {
        return null

    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }

}

Service에서는 onBind를 반드시 구현해야 하는데 만약 사용하지 않는다면 null을 반환한다.

 

이제 manifest에서 Service를 등록하고 ForegroundService의 permission을 등록해야한다.

<service android:name=".ForeService"></service> //서비스 등록
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission> //퍼미션 등록

위 두 코드를 넣는 위치는 아래와 같다.

 

<manifest>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <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.AlarmManagerForPosting"
        tools:targetApi="31">
        <service android:name=".ForeService"></service>
        <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>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

application 안에 service를 등록해준다.

 

메인 액티비티에서 버튼을 누르면 viewBinding을 사용하여 포어그라운드 서비스가 실행되도록 만들 예정이다.

 

viewBinding 사용법

https://growth-coder.tistory.com/30

 

[Android/Kotlin] activity와 fragment에서 view binding 사용법

view binding을 사용하면 view에 존재하는 값들에 접근할 수 있다. 이전에는 view에 존재하는 값들에 접근하기 위해서는 해당 ID를 사용해서 findViewById를 사용하였다. 이제는 view binding을 사용하면 쉽게

growth-coder.tistory.com

 

포어그라운드 서비스를 메인 액티비티에서 실행하려면 인텐트를 사용해야한다.

 

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.button.setOnClickListener {
            val intent = Intent(this, ForeService::class.java)
            ContextCompat.startForegroundService(this, intent)
        }
    }
}

이제 버튼을 누르면 포어그라운드 서비스가 실행된다.

 

ForeService에서는 어떠한 동작을 수행할 지 코드를 작성해주면 된다.

 

1에서 100까지 출력하고 진행과정을 notification으로 알려주어야 한다.

 

notification channel을 만들어야하는데 이는 안드로이드 최상위 객체인 Application 안에서 생성한다.

 

Application은 최상위 객체이기 때문에 여러 곳에서 공통적으로 사용하는 데이터들을 넣어주면 편리하게 사용이 가능하다.

 

Application을 상속받은 클래스 안에서 notification channel을 만드는 과정은 다음과 같다.

 

1. 채널 아이디를 상수로 만들어준다.

 

companion object 안에 채널 아이디를 상수로 만들어준다.

class App : Application(){
    companion object {
        const val FORE_CHANNEL_ID = "forenotification"
    }
}

App 안의 데이터들을 사용하기 위해서는 App을 manifest에 등록해줘야한다.

android:name=".App"

최종 manifest 파일

<?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>
    <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.ForeServicePosting"
        android:name=".App"
        tools:targetApi="31">
        <service android:name=".ForeService"></service>
        <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>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

 

2. notification channel을 생성한다.

class App : Application(){
    companion object {
        const val FORE_CHANNEL_ID = "forenotification"
    }
    override fun onCreate(){
        super.onCreate()
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            getSystemService(NotificationManager::class.java).run{
                val foreChannel = NotificationChannel(
                    FORE_CHANNEL_ID, //아이디
                    "Foreground", //이름
                    NotificationManager.IMPORTANCE_LOW //중요도. 높을수록 사용자에게 알리는 강도가 높아짐

                )
                createNotificationChannel(foreChannel)
            }
        }
    }
}

run은 영역함수로 다음 포스팅을 참고.

https://growth-coder.tistory.com/44

 

[Kotlin] 영역 함수 (run, let, with, apply, also)

코틀린에는 영역 함수라는 것이 존재한다. 영역 함수를 사용하면 특정 객체에 대한 식이라는 것을 알 수 있어서 가독성이 좋아진다. 영역 함수 모두 사용하는 목적은 비슷하지만 사용 방식에 있

growth-coder.tistory.com

 

이제 Service에서 notification channel을 바탕으로 notification builder를 만들어서 해당 notification의 출력 형태를 지정할 수 있다.

 

1. pending intent를 생성한다.

 

pending intent는 notification을 눌렀을 때 원하는 액티비티를 띄우려 할 때 사용한다.

val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
    this, //context
    0, //request code
    intent, //flag
    PendingIntent.FLAG_IMMUTABLE //flag

)

2. notification builder를 생성한다.

val notiBuilder = NotificationCompat.Builder(this,App.FORE_CHANNEL_ID)
    .setContentTitle("foreground")
    .setContentText("진행 상황 : 0")
    .setContentIntent(pendingIntent)

 

3. foreground 서비스를 시작한다.

startForeground(SERVICE_ID,notiBuilder.build())
//SERVICE_ID는 상수로 class 바깥에 전역 변수로 지정했다.
//const val SERVICE_ID=1

4. 서비스의 동작을 만든다.

GlobalScope.launch{
    val manager = getSystemService(NotificationManager::class.java)
    for(i in 1..100){
        notiBuilder.setContentText("진행 상황 $i")
        delay(100)
        manager.notify(SERVICE_ID, notiBuilder.build()) //변화를 알려줌
    }
    stopForeground(true)
    stopSelf()
}

5.플래그를 반환한다.


return START_NOT_STICKY

<ForeService.kt> 최종 코드

const val SERVICE_ID = 1
class ForeService : Service(){
    override fun onBind(p0: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val intent = Intent(this, MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            this, //context
            0, //request code
            intent, //flag
            PendingIntent.FLAG_IMMUTABLE //flag

        )
        val notiBuilder = NotificationCompat.Builder(this,App.FORE_CHANNEL_ID)
            .setContentTitle("foreground")
            .setContentText("진행 상황 : 0")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentIntent(pendingIntent)

        startForeground(SERVICE_ID,notiBuilder.build())

        GlobalScope.launch{
            val manager = getSystemService(NotificationManager::class.java)
            for(i in 1..100){
                notiBuilder.setContentText("진행 상황 $i")
                delay(100)
                manager.notify(SERVICE_ID, notiBuilder.build()) //변화를 알려줌
            }
            stopForeground(true)
            stopSelf()
        }


        return START_NOT_STICKY
    }

}
728x90
반응형

댓글