백그라운드로 처리할 작업을 위한 앱 컴포넌트
원칙적으로 사용자 인터페이스는 제공하지 않음
Started 서비스
시작 명령에 의해 시작하는 서비스로 단방향
Bound 서비스
양방향으로 상호작용 가능
Started와 Bound 서버를 혼합해서 사용하기도 함
<service
android:name=".MyService"
android:foregroundServiceType="shortService" />
onStartCommand() 재정의startForeground() 호출class MyService : Service() {
override fun onCreate() { // 서비스가 생성될 때 호출되는 콜백
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
// Android 8.0 이상일 때
createNotificationChannel() // 알림 채널 생성
}
}
override fun onStartCommand(intent: intent?, flags: Int, startId: Int): Int {
myStartForeground(notificationID, createNotification())
serviceScope.launch {
delay(1000) // 코루틴으로 1초 대기
for (i in 1..10) {
println("in service $startId#i")
myStartForeground(notificationID, createNotification(i * 10))
delay(1000)
}
stopSelf(startId)
}
return START_NOT_STICKY // 서비스가 강제 종료되면 재시작하지 않도록
}
}
foreground type을 지정해서 startForeground()를 호출해야 함private fun myStartForeground(id: Int, notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES_UPSIDE_DOWN_CAKE) {
startForeground(
id,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERIVE
// foreground serive type 지정
)
} else {
startForeground(id, notification)
}
}
class MainActivity : ComponentActivity() {
... 생략 ...
Button(onClick = {startForegroundService() }) {
Text("Start Service")
}
... 생략 ...
private fun startForegroundService() {
Intent(this, MyService::class.java).also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
// Android 8.0 이상부터는
// 앱이 백그라운드에 있으면 서비스 실행이 제한됨
// 포그라운드 서비스는 알림을 띄워야만 실행 가능
startForegroundService(it)
// 시스템에 포그라운드 서비스로 동작할 것이라는 예고를 보내는 함수
// 서비스 시작 + 시스템에 포그라운드 서비스로 예고
} else {
startService(it)
// 8.0 이전 버전에는 예고가 필요 없음
}
}
}
}
화면의 상단에 앱과 관련한 정보를 표시
알림을 만들기 위해
NotificationChannel 생성
Notification 생성
생성한 Notification을 알림창에 표시
액티비티에서는 NotificationManagerCompat의 Notify로 알림 표시
Foreground 서비스에서는 startForeground()로 알림 표시
안드로이드 13부터 알림 권한도 필요함
<uses-permission android:name="android.permission.POST_NOTIFICATION" />
동적 권한 요청: Manifest.permission.POST_NOTIFICATIONS
private val channelID = "default"
private val notificationID = 1
// 알림 채널 생성
@RequireApi(Build.VERSION_CODES.0)
private fun createNotificationChannel() {
val channel = NotificaitonChannel(channelID, "default channel",
NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "description text of this channel."
NotificationManagerCompat.from(this).createNotificationChannel(channel)
}
}
// 알림 생성
private fun createNotification(progress: Int = 0)
= NotificationCompat.Builder(this, channelID)
.setContentTitle("Downloading")
.setContentText("Downloading a file from a cloud")
.setSmallIcon(R.drawable.ic_baseline_cloud_download_24)
.setOnlyAlertOnce(true) // 알람 소리가 날 때, 처음에만 나게 함
.setProgress(100, progress, false)
.build()
// 알림 표시 또는 업데이트
private fun showNotification(id: Int, notification: Notification) {
if (AvtivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
NotificationManagerCompat.from(this).notify(id, notification)
}
}
클라이언트-서버 방식에서 서버에 해당
클라이언트는 액티비티 등이 되고, 다른 앱의 컴포넌트도 될 수 있음
다른 앱의 컴포넌트가 클라이언트가 된다면
IPC(Inter Process Communication)가 됨
Binder를 상속하여 만들기
Messager나 AIDL 사용
class MyService : Service() {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService() = this@MyService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
var startedCount = 0
private set
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
println("MyService:onStartCommand $startId")
startedCount++
return super.onStartCommand(intent, flags, startId)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
BackgroundTasksTheme {
Surface( ... ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
RequestPermission()
MainScreen( onGetServiceCount = { myService?.startedCount ?: 0 }, ... )
}
}
}
}
private var myService: MyService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
myService = (service as MyService.LocalBinder).getService()
// 바운드됨
}
override fun onServiceDisconnected(name: ComponentName?) {
myService = null
}
}
override fun onStart() {
super.onStart()
Intent(this, MyService::class.java).also {
bindService(it, serviceConnection, BIND_AUTO_CREATE)
// Bound 서비스 bind, 결과가 serviceConnection으로 전달
}
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
}
}
설치 시 권한 부여
동적 권한 부여
dangerous라고 표시되어 있음Manifest.permission.FOREGROUND_SERVICE_X: Foreground 서비스
Manifest.permission.POST_NOTIFICATIONS: 알림 표시 권한
AndroidManifest.xml에 권한을 표시하고 설치시에 사용자에게 확인
앱이 실행 중에 권한이 필요할 때 사용자에게 권한 부여 요청
동적 권한 요청하기 전에 사용자에게 권한의 필요성을 설명하도록 권장
<uses-permission android:name=
"android.permission.FOREGROUND_SERVICE_DATA_STNC" />
<uses-permission android:name=
"android.permission.POST_NOTIFICATIONS" />
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun RequestPermission() {
val context = LocalContext.current
val permission = Manifest.permission.POST_NOTIFICATIONS
var showRationale by remember { mutableStateOf(false) }
var showWarning bt remember { mutableStateOf(false) }
val requestPermLauncher = rememberLauncherForActivityResult
(ActivityResultContracts.RequestPermission()) {
isGranted -> if (!isGranted) showWarning = true
}
LaunchedEffect(Unit) {
permission.let {
if (context.checkSelfPermission(it) !=
PackageManager.PERMISSION_GRANTED) {
if ((context as? ComponentActivity)?.shouldShowRequestPermissionRationale(it) == true) {
showRationale = true
} else {
requestPermLauncher.launch(it)
}
}
}
}
if (showRationale) {
AlertDialog(
onDismissRequest = { showRationale = false },
title = { Text("Notification Permission") },
text = { Text(stringResource
(R.string.req_permission_reason, "Notifications")) },
confirmButton = {
TextButton(onClick = {
showRationale = false
requestPermLauncher.launch(permission)
}) { Text("Allow") }
},
dismissButton = {
TextButton(onClick = { showRationale = false }) { Text("Deny") }
}
)
}
if (showWarning) {
AlertDialog(
onDismissRequest = { showWarning = false },
title = { Text("Permission Denied") },
text = { Text(stringResource
(R.string.no_permission, "Notifications")) },
confirmButton = {
TextButton(onClick = {showWarning = false}) { Text("OK") }
}
)
}
}
Foreground Service
오래 백그라운드로 작업을 해야 하는 경우
Work Manager
주기적인 작업이나 기타 다른 백그라운드 작업 대부분
앱이 종료되거나 디바이스가 재부팅되더라도
실행이 보장되는 백그라운드 작업
백엔드 서비스로 로그 등을 보내기, 주기적으로 서버와 데이터 동기화
특정 조건(네트워크나 저장 공간)이 만족될 때만 실행 가능하도록 할 수 있음
WorkManager
WorkManager.getInstance(컨텍스트)Worker
doWork()를 재정의하여 할 일을 정의함WorkRequest
Worker와 제약 조건 등을 기술한 요청 객체WorkManager에게 WorkRequest 객체를 전달하여 작업 시작OneTimeWorkRequest, PeriodWorkRequestclass MyWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
setForeground(createForegroundInfo("Starting Download"))
// worker를 foreground 서비스로 실행하도록 설정
// 일반적인 foreground 작업에서는 호출하지 않아도 되지만,
// 오래 걸리는 작업에서는 foregroundService처럼 사용하기 위해 호출해야 함
// startForeground(notificationId, notification)와 역할이 동일함
for (i in 1..10) {
delay(1000)
setForeground(createForegroundInfo("Downloading ${i * 10}%"))
// 알림 업데이트
}
return.Result.success()
}
companion object {
const val name = "com.example.backgroundtasks.MyWorker"
}
}
Manifest에 FORGROUND_SERVICE_DATA_SYNC 권한 추가
<uses-permission android:name=
"android.permission.FORGROUND_SERVICE_DATA_SYNC" />
Manifest에 <service> 추가
WorkManager가 내부적으로 사용하는 서비스 등록foregroundServiceType="dataSync"는 해당 서비스가<service android:name=
"androidx.work.impl.foreground.SystemForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />
doWork() 에서 setForeground(foregrondInfo) 호출foregroundInfo는 알림을 만드는 것, 아래와 같이 만들 수 있음private fun createForegroundInfo(progress: String): ForegroundInf {
val title = "Playing Music"
val cancel = "Cancel Task"
val intent =
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
createNotificationChannel()
}
val notification =
NotificationCompat.Builder(applicationContext, channelID)
.setContentTitle(title).setContentText(progress)
.setSmallIcon(R.drawable.baseline_music_note_24)
.setOngoing(true).setOnlyAlertOnce(true)
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ForegroundInfo(notificationID, notification,
FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
ForegroundInfo(notificationID, notification)
}
}
val oneTimeRequest = OneTimeRequestBuilder<MyWorker>() // 일회성 작업
.setConstraints(constraints) // 제약 조건 부여
.build() // 빌드, workrequest 객체 생성/리턴
WorkManager.getInstance(this).enqueueUniqueWork(
MyWorker.name, // MyWorker에서 정의한 식별자용 이름
ExistingWorkPolicy.KEEP, // 동일한 Worker가 있을 때 처리 방법
// KEEP은 기존 것을 유지
oneTimeRequest)
)
val repeatingRequest =
PerioidWorkRequestBuilder<MyWorker>(15, TimeUnit.MINUTES) // 15분 주기
.setConstraints(constraints) // 제약 조건 부여
.build() // 빌드, workrequest 객체 생성/리턴
WorkManager.getInstance(this).enqueueUniqueWork(
MyWorker.name, // MyWorker에서 정의한 식별자용 이름
ExistingWorkPolicy.KEEP, // 동일한 Worker가 있을 때 처리 방법
// KEEP은 기존 것을 유지
oneTimeRequest)
val constraints = Constraints.Builder().apply {
setRequiredNetworkType(NetworkType.UNMENTERED) // Wi-Fi
setRequiresBatteryNotLow(true) // 배터리가 적을 때
// setRequiresCharging(true) // 충전 중 일 때
// setRequiresDeviceIdle(true) // 유휴상태일 때
}.build()
cancelUniqueWork()WorkManager.getInstance(this).cancelUniqueWork(MyWorker.name)
Worker 식별자 이름으로 현재 Work Info를 가져옴(Flow를 통해)
Worker 상태
ENQUEUED: worker가 큐에 들어가서 대기 중, 시간이 되면 시작함RUNNING: worker 실행 중SUCCEEDED: worker 실행 완료/성공 OneTimeWorker만 해당됨CANCELED: worker 취소됨val workInfos by WorkManager.getInstance(context).
getWorkInfosForUniqueWorkFlow(MyWorker.name).
collectAsStateWithLifecycle(initialValue = emptyList())
액티비티 시작
Application 시작에서 worker 시작
MyApplication: Application()으로 만들고 onCreate() 재정의하여
onCreate()에서 Worker시작*
MyApplication은 AndroidManifest에 등록해야 함
Worker는 백그라운드로 실행하므로 UI 관련 코드는 없어야 함
Application 클래스와 Manifest에 등록import android.app.Application
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
startWorker()
}
private fun startWorker() {
// ... 생략 ...
}
}
<application android:name=".MyApplication">
</application>