들어가기에 앞서
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가 실행되어 MusicPlayerService
의 onStartCommand()
에 전달되겠죠!
아래는 알림창의 각 버튼들을 눌렀을때 뜨는 로그입니다.
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
}
}
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>
'프로그래밍 > Android' 카테고리의 다른 글
[안드로이드] 주요 이미지 라이브러리 메모리 사용량 비교해보기! (Glide vs Picasso vs Coil) (1) | 2021.01.02 |
---|---|
잘 정리된 코틀린 코루틴 (0) | 2020.12.23 |
[안드로이드] 서비스(Service)에 대해 알아보자 (0) | 2020.10.13 |
[안드로이드] Proto DataStore 사용법 (2) | 2020.10.10 |
[Android] Preferences DataStore 사용법과 개념 (0) | 2020.10.09 |