슬립나우: 수면시간 추천 및 계산 - Google Play 앱
슬립나우로 적절한 시간에 알람을 맞춰 개운하게 일어나세요.
play.google.com
개요
여러 프로세스를 실행하거나 앱이 꺼져도 계속해서 어떠한 동작을 실행 시키고 싶을 때가 있다.
그럴 때 사용하는 기능이 포그라운드 서비스다.
그럼 포그라운드와 백그라운드의 차이는 무엇인가?
포그라운드 vs 백그라운드
1. 포그라운드(Foreground) - 죽지않는 서비스
포그라운드는 사용자가 인지할 수 있는 작업을 수행한다.
포그라운드 서비스를 실행하기 위해서는 상태바 알림을 꼭 띄어야만 한다.
백그라운드와는 다르게 메모리 부족 시에도 시스템에서 강제로 서비스를 종료하지 않는다.
ex) 앱 다운로드, 노래 재생 등
2. 백그라운드(Background)
백그라운드는 사용자에게 직접적으로 보이지 않는 작업을 수행한다.
메모리 부족 시 서비스가 강제 종료된다는 단점이 있다.
구현
1. Layout 구성
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">
<Button
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="Start Service"
android:id="@+id/btn_start"/>
<Button
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="Stop Service"
android:id="@+id/btn_stop"/>
</LinearLayout>
2. Service 생성


<AndroidManifest.xml>
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
Service 생성 시 manifest 파일에 service 태그가 생성된 것을 확인할 수 있다.
<AndroidManifest.xml>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
Foreground 권한을 추가한다.
3. 버튼 클릭 리스너
<MainActivity.kt>
package com.example.foregroundexample
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
//Button
var btn_start: Button? = null
var btn_stop: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_start = findViewById(R.id.btn_start)
btn_stop = findViewById(R.id.btn_stop)
btn_start!!.setOnClickListener(View.OnClickListener {
val serviceIntent = Intent(this@MainActivity, MyService::class.java)
startService(serviceIntent)
Toast.makeText(this@MainActivity, "Service start", Toast.LENGTH_SHORT).show()
})
btn_stop!!.setOnClickListener(View.OnClickListener {
val serviceIntent = Intent(this@MainActivity, MyService::class.java)
stopService(serviceIntent)
Toast.makeText(this@MainActivity, "Service stop", Toast.LENGTH_SHORT).show()
})
}
}
4. Notification 구현
<MyService.kt>
private fun createNotification() {
val builder = NotificationCompat.Builder(this, "default")
builder.setSmallIcon(R.mipmap.ic_launcher)
builder.setContentTitle("Foreground Service")
builder.setContentText("포그라운드 서비스")
builder.color = Color.RED
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
builder.setContentIntent(pendingIntent) // 알림 클릭 시 이동
// 알림 표시
val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
"default",
"기본 채널",
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
notificationManager.notify(NOTI_ID, builder.build()) // id : 정의해야하는 각 알림의 고유한 int값
val notification = builder.build()
startForeground(NOTI_ID, notification)
}
Foreground 서비스를 실행시키기 위해서는 사용자가 서비스가 실행되고 있다는 것을 알게 해야하기 때문에,
푸시 알림을 띄워준다.
5. Thread 구현
<MyService.kt>
private var mThread: Thread? = object : Thread("My Thread") {
override fun run() {
super.run()
for (i in 0..99) {
Log.d(TAG, "count : $i")
try {
sleep(1000)
} catch (e: InterruptedException) {
currentThread().interrupt()
break
}
}
}
서비스가 작동하는 것을 확인하기 위해서 스레드를 생성해 count 값을 증가시켜 확인해보겠다.
6. Service Method
<MyService.kt>
override fun onCreate() {
super.onCreate()
createNotification()
mThread!!.start()
Log.d(TAG, "onCreate")
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
if (mThread != null) {
mThread!!.interrupt()
mThread = null
}
Log.d(TAG, "onDestroy")
}
onCreate()
: 서비스가 최초 생성될 때 한번 호출됨onStartCommand()
: 앱의 다른 구성 요소에서 서비스 실행 시 함수 호출onDestroy()
: 서비스 소멸 시 호출
7. 결과







8. 전체코드
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">
<Button
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="Start Service"
android:id="@+id/btn_start"/>
<Button
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="Stop Service"
android:id="@+id/btn_stop"/>
</LinearLayout>
<AndroidManifest.xml>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.foregroundexample">
<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/Theme.ForegroundExample">
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></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>
</activity>
</application>
</manifest>
<MainActivity.kt>
package com.example.foregroundexample
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
//Button
var btn_start: Button? = null
var btn_stop: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_start = findViewById(R.id.btn_start)
btn_stop = findViewById(R.id.btn_stop)
btn_start!!.setOnClickListener(View.OnClickListener {
val serviceIntent = Intent(this@MainActivity, MyService::class.java)
startService(serviceIntent)
Toast.makeText(this@MainActivity, "Service start", Toast.LENGTH_SHORT).show()
})
btn_stop!!.setOnClickListener(View.OnClickListener {
val serviceIntent = Intent(this@MainActivity, MyService::class.java)
stopService(serviceIntent)
Toast.makeText(this@MainActivity, "Service stop", Toast.LENGTH_SHORT).show()
})
}
}
<MyService.kt>
package com.example.foregroundexample
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
class MyService : Service() {
override fun onBind(intent: Intent): IBinder? {
// TODO: Return the communication channel to the service.
throw UnsupportedOperationException("Not yet implemented")
}
override fun onCreate() {
super.onCreate()
createNotification()
mThread!!.start()
Log.d(TAG, "onCreate")
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
private var mThread: Thread? = object : Thread("My Thread") {
override fun run() {
super.run()
for (i in 0..99) {
Log.d(TAG, "count : $i")
try {
sleep(1000)
} catch (e: InterruptedException) {
currentThread().interrupt()
break
}
}
}
}
private fun createNotification() {
val builder = NotificationCompat.Builder(this, "default")
builder.setSmallIcon(R.mipmap.ic_launcher)
builder.setContentTitle("Foreground Service")
builder.setContentText("포그라운드 서비스")
builder.color = Color.RED
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
builder.setContentIntent(pendingIntent) // 알림 클릭 시 이동
// 알림 표시
val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
"default",
"기본 채널",
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
notificationManager.notify(NOTI_ID, builder.build()) // id : 정의해야하는 각 알림의 고유한 int값
val notification = builder.build()
startForeground(NOTI_ID, notification)
}
override fun onDestroy() {
super.onDestroy()
if (mThread != null) {
mThread!!.interrupt()
mThread = null
}
Log.d(TAG, "onDestroy")
}
companion object {
private const val TAG = "MyServiceTag"
// Notification
private const val NOTI_ID = 1
}
}
마무리
백그라운드에서 동작하는 Foreground 서비스에 대해서 알아보았다.
다음 포스팅에서는 Foreground 서비스를 응용하여 STT변환 기능을 구현해보도록 하겠다.
GitHub - jaemin-Yoo/continuous-voice-recognition: Implementation of continuous speech recognition using SpeechRecognizer api
Implementation of continuous speech recognition using SpeechRecognizer api - GitHub - jaemin-Yoo/continuous-voice-recognition: Implementation of continuous speech recognition using SpeechRecognizer...
github.com
'개발 > Kotlin & Android' 카테고리의 다른 글
[Kotlin & Android] 코틀린 for 문 사용법 (0) | 2021.12.12 |
---|---|
[Kotlin & Android] 코틀린 전역 변수 선언 (다른 엑티비티 변수 사용하기) (0) | 2021.12.01 |
[Kotlin & Android] 호출한 엑티비티에서 값 받기(startActivityForResult 함수 deprecated) (0) | 2021.11.30 |
[Kotlin & Android] Android 지속적인 음성인식 기능 구현 (SpeechRecognizer) (0) | 2021.10.28 |
[Kotlin & Android] Android SpeechRecognizer API 구현하기 (음성을 텍스트로 변환 - STT변환) (0) | 2021.10.28 |