프로그래밍/Android

[안드로이드] 예제로 알아보는 Foreground Service

Lou Park 2020. 10. 15. 15:53

들어가기에 앞서


Foreground Service를 어떻게 이용하면 좋을까요? 간단한 예제를 통해서 알아보도록 하겠습니다.
예제로 만들어 볼 앱은 가짜 음악 플레이어 앱입니다. 멜론이나 벅스, 지니 등 음악 앱을 이용하면 상단 알림창에 현재 재생중인 음악이 뜹니다. 그리고 다음곡이나 이전곡, 재생 및 일시정지가 가능한데요, 이것은 Foreground Service로 구현할 수 있습니다.

또 다른 예시로는 현재 유저가 걷는 걸음이나 거리를 측정하는 피트니스 앱이 있겠네요.

 

서비스에 대해 궁금하시다면 Service 전반에 대한 내용을 다룬 이전 글을 참조 해 주세요.

 

MusicPlayer Example


activity_main.xml

사진과 같은 레이아웃을 만들어 줍니다.
Start Foreground Service 버튼을 누르면 Service가 시작되고, Stop Foreground Service를 누르면 Service가 중지되도록 할 것 입니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Main Activity"/>

    <Button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="26dp"
        android:text="Start Foreground Service" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="35dp"
        android:text="Stop Foreground Service" />

</LinearLayout>

 

 

 

 

MusicPlayerService

 

 

onStartCommand()
Service 클래스를 상속하여 MusicPlayerService를 만들어 줍니다.
onStartCommand() 메소드를 보시면 START_STICKY라는 상수값을 리턴하고 있습니다. 각 상수 값들에 대한 설명은 다음과 같습니다.

설명
START_STICKY 서비스가 죽어도 시스템에서 다시 재생성합니다.
START_NOT_STICKY 서비스가 죽어도 시스템에서 재생성하지 않습니다.

 

 

Actions.kt
상수 값들을 정리해놓은 Actions Object입니다. Intent에 원하는 Action값을 실어보내면 onStartCommand()에서 Action 값에 따라 필요한 액션을 취하고 있습니다.

object Actions {
    private const val prefix = "com.gold24park.musicplayerexample.action."
    const val MAIN = prefix + "main"
    const val PREV = prefix + "prev"
    const val NEXT = prefix + "next"
    const val PLAY = prefix + "play"
    const val START_FOREGROUND = prefix + "startforeground"
    const val STOP_FOREGROUND = prefix + "stopforeground"
}

 

 

startForegroundService()
Foreground Service는 유저가 현재 휴대폰에서 Foreground Service가 돌아가고 있고, 시스템 자원을 사용하고 있다는 사실을 알 수 있도록 해아합니다. 그래서 상태바의 알림(Notification)을 이용합니다. Foreground Service로 실행되는 알림은 Foreground Service가 종료되지 않은채로 지울 수 없습니다.

코드상에서는 MusicNotification이라는 커스텀 클래스를 만들어 Notification 객체를 따로 생성해서 받아 오고 있습니다. 그리고 startForeground()Foreground Service를 실행하게 됩니다. 그러면 알림이 휴대폰에 뜨게 되죠.
NOTIFICATION_ID에는 Int값이 들어가는데, 0보다 큰 양수로서 상태바의 알림을 구분해주는 고유 값이 됩니다.

stopForegroundService()
Foreground에 있는 Service를 종료시키기 위해서는 stopForeground()라는 메소드를 호출 해야 합니다. 여기에는 하나의 Boolean 인자가 들어가는데요, 상태바의 알림도 같이 종료할지에 대한 Boolean 인자입니다. true값을 넘길경우 Foreground Service 종료 시 상태바의 알림도 사라지게 됩니다. 그리고 stopSelf() 메소드를 통해 MusicPlayerService 역시 중단시킵니다.

class MusicPlayerService : Service() {

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.e(TAG, "Action Received = ${intent?.action}")
        // intent가 시스템에 의해 재생성되었을때 null값이므로 Java에서는 null check 필수
        when (intent?.action) {
            Actions.START_FOREGROUND -> {
                Log.e(TAG, "Start Foreground 인텐트를 받음")
                startForegroundService()
            }
            Actions.STOP_FOREGROUND -> {
                Log.e(TAG, "Stop Foreground 인텐트를 받음")
                stopForegroundService()
            }
            Actions.PREV -> Log.e(TAG, "Clicked = 이전")
            Actions.PLAY -> Log.e(TAG, "Clicked = 재생")
            Actions.NEXT -> Log.e(TAG, "Clicked = 다음")
        }
        return START_STICKY
    }

    private fun startForegroundService() {
        val notification = MusicNotification.createNotification(this)
        startForeground(NOTIFICATION_ID, notification)
    }

    private fun stopForegroundService() {
        stopForeground(true)
        stopSelf()
    }

    override fun onBind(intent: Intent?): IBinder? {
        // bound service가 아니므로 null
        return null
    }

    override fun onCreate() {
        super.onCreate()
        Log.e(TAG, "onCreate()")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.e(TAG, "onDestroy()")
    }

    companion object {
        const val TAG = "[MusicPlayerService]"
        const val NOTIFICATION_ID = 20
    }

}

 

 

MusicNotification.kt

다음은 Notification을 생성해보겠습니다. Notification.Builder에서 addAction() 메소드를 이용하면 알림에 버튼을 넣을 수 있습니다. Android N 이후로는 addAction에서 아이콘을 정의하더라도 보이지 않고 사진처럼 Title만 보이게됩니다. 각각의 버튼을 누르게 되면, 정의 해둔 PendingIntent가 작동합니다. 예를들어 "Prev" 버튼을 눌렀을때는 Actions.PREV 값이 담긴 Intent가 실행되어 MusicPlayerServiceonStartCommand()에 전달되겠죠!

알림창의 모습

아래는 알림창의 각 버튼들을 눌렀을때 뜨는 로그입니다.

object MusicNotification {
    const val CHANNEL_ID = "foreground_service_channel" // 임의의 채널 ID

    fun createNotification(
        context: Context
    ): Notification {
        // 알림 클릭시 MainActivity로 이동됨
        val notificationIntent = Intent(context, MainActivity::class.java)
        notificationIntent.action = Actions.MAIN
        notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or 
            Intent.FLAG_ACTIVITY_CLEAR_TASK

        val pendingIntent = PendingIntent
            .getActivity(context, 0, notificationIntent, FLAG_UPDATE_CURRENT)

        // 각 버튼들에 관한 Intent
        val prevIntent = Intent(context, MusicPlayerService::class.java)
        prevIntent.action = Actions.PREV
        val prevPendingIntent = PendingIntent
            .getService(context, 0, prevIntent, 0)

        val playIntent = Intent(context, MusicPlayerService::class.java)
        playIntent.action = Actions.PLAY
        val playPendingIntent = PendingIntent
            .getService(context, 0, playIntent, 0)

        val nextIntent = Intent(context, MusicPlayerService::class.java)
        nextIntent.action = Actions.NEXT
        val nextPendingIntent = PendingIntent
            .getService(context, 0, nextIntent, 0)

        // 알림
        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setContentTitle("Music Player")
            .setContentText("My Music")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setOngoing(true) // true 일경우 알림 리스트에서 클릭하거나 좌우로 드래그해도 사라지지 않음
            .addAction(NotificationCompat.Action(android.R.drawable.ic_media_previous, 
                "Prev", prevPendingIntent))
            .addAction(NotificationCompat.Action(android.R.drawable.ic_media_play, 
                "Play", playPendingIntent))
            .addAction(NotificationCompat.Action(android.R.drawable.ic_media_next, 
                "Next", nextPendingIntent))
            .setContentIntent(pendingIntent)
            .build()


        // Oreo 부터는 Notification Channel을 만들어야 함
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                CHANNEL_ID,
                "Music Player Channel", // 채널표시명
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = context.getSystemService(NotificationManager::class.java)
            manager?.createNotificationChannel(serviceChannel)
        }

        return notification
    }
}

 

NotificationChannel 의 채널 표시명은 각 앱의 알림설정에서 볼 수 있습니다.

 

 

MainActivity.kt

MainActivity에서는 MusicPlayerService를 실행하고, 종료하는 코드를 넣어주면됩니다.
각 버튼을 눌렀을 때 뜨는 로그는 다음과 같습니다.

btn_start 클릭 시

서비스가 실행된 채로 여러번 클릭하면 onCreate()는 호출되지 않습니다.

btn_stop 클릭 시

stopSelf()를 했기때문에 서비스가 종료되면서 onDestroy()까지 호출됩니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_start.setOnClickListener {
            val intent = Intent(this@MainActivity, MusicPlayerService::class.java)
            intent.action = Actions.START_FOREGROUND
            startService(intent)
        }

        btn_stop.setOnClickListener {
            val intent = Intent(this@MainActivity, MusicPlayerService::class.java)
            intent.action = Actions.STOP_FOREGROUND
            startService(intent)
        }
    }
}

 

 

AndroidManifest.xml

안드로이드 9(API Level 28)이상 부터는 Foreground service를 사용하기 위해 FOREGROUND_SERVICE 권한을 매니피스트에 명시 해 주어야 합니다. 일반(normal) 권한으로, 선언만 해두면 시스템에서 자동적으로 승인 해줍니다. 만일 이 권한이 선언되어 있지 않은 상태에서 Foreground Service를 띄우게 되면 SecurityException이 뜨게됩니다.

그리고 우리가 만든 Service를 선언해주는 것도 빠뜨리지 않기!

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

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".service.MusicPlayerService"/>
    </application>

</manifest>