# 공부하게된 이유
FCM 알림을 받던 도중 포그라운드 서비스를 시작하는 이슈가 발생!
Android 12 이상을 타겟팅하는 앱은 특별한 사례 몇 가지를 제외하고 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 없습니다.
앱이 백그라운드에서 실행되는 동안 WorkManager를 사용하여 신속 처리 작업을 예약하고 시작해 보세요.
신속히 처리해야 하는 사용자 요청 작업을 완료하려면 정확한 알람 내에서 포그라운드 서비스를 시작하세요.
앱 성능과 UX를 개선하기 위해 Android 12 이상을 타겟팅하는 앱은 알림 트램펄린으로 사용되는 서비스나 broadcast receiver에서 활동을 시작할 수 없습니다. 즉, 사용자가 알림을 탭하거나 알림 내에서 작업 버튼을 탭한 후, 앱은 서비스나 broadcast receiver 내부에서 startActivity()를 호출할 수 없습니다.
# WorkManager
- 앱이 종료되거나 기기가 다시 시작되어도 실행 예정인 지연 가능한 비동기 작업을 쉽게 예약할 수 있게 해준다
- 안드로이드의 백그라운드 작업을 처리하는 방법 중 하나, Android Jetpack 아키텍처의 구성 요소 중 하나이다
- 하나의 코드로 API Level 마다 비슷한 동작을 보장한다
프로세스 종료 여부와 관계없이 반드시 작업을 실행한다.
하지만 작업이 즉시 실행되는 것을 보장하지 않음.
# 주요 기능
- API 14 이상 단말을 지원한다
- 네트워크 가용성, 충전상태와 같은 작업의 제약 조건을 설정할 수 있다
- 일회성 혹은 주기적인 비동기 작업을 예약할 수 있다
- 예약된 작업 모니터링 및 관리
- 작업 체이닝
- 앱이나 기기가 다시 시작되는 경우에도 작업 실행을 보장한다
- 잠자기 모드와 같은 절전 기능을 지원한다
- WorkManager는 앱이 종료되거나 기기가 다시 시작되는 경우에도 지연 가능 하고 안정적으로 실행되어야 하는 작업을 대상으로 설계되어 있다
- 백엔드 서비스에 로그 또는 분석을 전송하는 작업
- 주기적으로 애플리케이션 데이터를 서버와 동기화 하는 작업
# WorkManager 구성
- Worker
- Abstract class
- 클래스를 상속받고 백그라운드에서 실행하고자 하는 코드를 doWork() 메소드에 정의한다.
- 작업 상태를 나타내는 Result에 정의된 success(), failure(), retry() 등의 메소드를 통해 결과를 반환한다.
- WorkRequest
- Work에 정의된 Task를 작동시키기 위한 request를 나타낸다.
- WorkRequest를 생성할 때 반복 여부, 제약 사항 등의 정보를 담는다.
- OneTimeWorkRequest
- 한번만 실행할 작업 요청
- PeriodicWorkRequest
- 일정 주기로, 여러번 실행할 작업 요청을 나타내는 WorkRequest
- WorkManager
- 실제로 WorkRequest를 스케줄링하고 실행하며 관리하는 클래스
- 인스턴스를 받아와 WorkRequest를 큐에 추가하여 실행하도록 한다.
# 구현하기
Dependency 추가
dependencies {
implementation "androidx.work:work-runtime-ktx:2.5.0" // Kotlin + Coroutines
}
Worker 작성하기
Worker를 상속받은 클래스를 생성하고 Context와 WorkerParameters를 생성자에 넘긴다.
doWork()메소드를 오버라이드하고, 하고자 하는 작업 코드를 작성한다.
class SomeWorker(ctx: Context, params: WorkerParameters) :
Worker(ctx, params) { // If you want coroutines, CoroutineWorker()
override fun doWork(): Result {
return try {
Timber.d("I'm hard worker, but i will sleep") // Timber is library for logging
Thread.sleep(3_000)
Timber.d("Who woke up. I'm tired.")
Result.success() // return statement
} catch (e: Exception) {
Timber.e("Worker Exception $e")
Result.failure() // return statement
}
}
}
WorkerRequest 작성하기
한번만 실행될 작업은 OneTimeWorkRequest, 반복적으로 실행될 작업은 PeriodicWorkRequest를 사용하여 만든다.
PeriodicWorkRequest의 경우 반복 주기 및 시간 단위를 인자로 넘겨야 하는데 최소값은 15분으로 정의되어 있다.
// in view(activity, fragment) or viewmodel
// OneTimeWorkRequest
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>().build()
// PeriodicWorkRequest
val workRequest = PeriodicWorkRequestBuilder<SomeWorker>(15, TimeUnit.MINUTES).build()
WorkerManager작성 및 큐에 작업 추가하기
WorkMager의 인스턴스를 getInstance() 메소드를 통해서 가져오고,
enqueue() 메소드를 통해 WorkRequest를 추가할 수 있다.
// in view(activity, fragment) or viewmodel
private val workManager = WorkManager.getInstance(application)
workManager.enqueue(workRequest) // 위에서 작성한 workRequest를 넣어준다.
# 더 알아보기
Constraint(제약 사항)
WorkRequestBuilder에 제약 사항을 추가할 수 있다.
Constraints.Builder()를 통해 작성.
- 네트워크 유형
- 충전 상태
등등..
// in view(activity, fragment) or viewmodel
val constraints = Constraints.Builder()
.setRequiresCharging(true) // 충전 상태일 때만 작동한다.
.setRequiresBatteryNotLow(true) // 배터리 부족상태가 아닐 때만 작동한다.
.build()
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.setConstraints(constraints)
.build()
태그 추가 및 작업 관리하기
모든 작업 요청에 고유 식별자인 태그를 넣어 해당 작업을 취소하거나 진행 상황을 확인할 수 있다.
WorkRequestBuilder에 addTag() 메소드를 통해 태그를 작성하여 식별할 수 있도록 만들어 준다.
태그 값을 통하여 취소는 간단하게 할 수 있으며 작업 진행 상황은 반환되는 WorkInfo객체를 통해 확인 할 수 있다.
- WorkInfo State
- BLOCKED
- CANCELLED
- ENQUEUED
- FAILED
- RUNNING
- SUCCEEDED
해당 객체는 LiveData로 반환 받을 수 있으며, 상태에 따라서 progress를 보여주는 등 다양한 작업을 처리할 수 있다.
// add tag
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.addTag("some_worker") // Add this method
.build()
// cancel work
workManager.cancelAllWorkByTag("some_worker")
// get work info to livedata
val workInfos = workManager.getWorkInfosByTagLiveData("some_worker")
// observe example in view
workInfos.observe(lifecycleOwner) { workInfos ->
if (listOfWorkInfo.isNullOrEmpty()) return@observe
val workInfo = workInfos[0]
if (workInfo.state.isFinished) { // SUCCEEDED or FAILED or CANCELLED
hideProgressBar()
} else {
showProgressBar()
}
}
여러 작업 체이닝 하기
작업을 순차적 또는 동시에 실행되도록 만드는 WorkRequest를 생성할 수 있다.
Worker들을 WorkRequest로 만들어 준다.
enqueue() 메소드 내에 WorkRequest를 보내는 것이 아니라, beginWith() 메소드를 통해 시작할 작업을 넘긴다.
이 때 두개이상의 작업을 동시에 시작하려면 List에 담아 넘겨준다.
위 작업이 끝나면 시작할 작업들을 then()을 통해 작성한다.
then()을 통해 계쏙 체이닝 할 수 있다.
작업 큐에 보낼 테스크를 모두 작성하였으면, enqueue()를 통해 큐에 최종적으로 추가하여 작업을 실행.
val work1 = OneTimeWorkRequestBuilder<Work1Worker>().build()
// ... val work2 ~
// workManager is WorkManager Instance.
workManager
.beginWith(listOf(work1, work2))
.then(work3)
.then(work4)
.enqueue()
Worker와 데이터 주고받기
Key-Value 쌍으로 이루어진 Worker의 Data로 데이터를 담아 Worker로 보낼 수 있으며 작업이 완료된 후 데이터를 받아올 수 있다.
WorkRequest에 setInputData()를 통해 Data를 보낼 수 있으며, 이때 Data를 만들 때에는 Data.Builder()를 사용한다.
Worker클래스의 생성자인 WorkerParameters로 데이터는 전달되며, getInputData()를 통해 쉽게 받아올 수 있다.
작업을 완료한 후에 Result.success()에 Data를 전달한다.
Data를 만들 때 Data.Builder()를 편하게 작성해주는 함수인 workDataOf()도 존재하며, 아래 Worker클래스에서 확인!
빌더를 통한 생성은 WorkRequest에 Input할 때 확인 할 수 있다.
Worker에서 전달된 데이터는 WorkInfo에서 확인할 수 있다.
// input data for worker
val inputData = Data.Builder().apply {
putString("secret_key", "genius-dev")
build()
}
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.setInputData(inputData)
.build()
workManager.enqueue(workRequest)
// in Worker
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val secretKey = inputData.getString("secret_key")
//..
val outputData = workDataOf("public_key" to "android")
Result.success(outputData)
}
}
// in view(activity, fragment) or viewmodel
workInfos.observe(lifecycleOwner) { workInfos ->
if (listOfWorkInfo.isNullOrEmpty()) return@observe
val workInfo = workInfos[0]
if (workInfo.state.isFinished) {
hideProgressBar()
val outputData = workInfo.outputData.getString("public_key") // <<<< HERE
Toast.makeText(this, "worker output data : $outputData", Toast.LENGTH_SHORT).show()
} else {
showProgressBar()
}
}
# 실제 적용!
1. WorkManager 클래스 선언
class CallWorkManager(
val context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
return try {
when (inputData.getString(타입 키값)) {
서비스 종료 키값 -> {
context.stopService(
Intent(context, Service::class.java)
)
}
서비스 실행 키값 -> {
Intent(context, Service::class.java).apply {
action = RingService.ACTION_START_FOREGROUND
putExtra(PARAM_DATA, inputData.getString(PARAM_DATA))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(this)
} else {
context.startService(this)
}
}
}
}
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.failure()
}
}
}
2. 푸시 왔을 때 실행!
Data inputData = new Data.Builder()
.putString(타입 키값, 실행할 타입 값)
.build();
WorkRequest workRequest =
new OneTimeWorkRequest.Builder(CallWorkManager.class)
.setInputData(inputData)
.build();
WorkManager.getInstance(MyApp.get()).enqueue(workRequest);
# 작업하면서 알게 된 점
ForegroundService를 통해 Notification을 띄울 경우
seFullScreenIntent에 의해서 액티비티가 실행된다.(시스템 UI가 전체 화면 Notification을 띄울 건지, Heads-up Notification을 띄울 건지 선택)
https://shwoghk14.blogspot.com/2020/12/android-notification-with-full-screen.html
# 액티비티의 화면을 강제로 Screen On 시키고 싶을 경우
액티비티 onCreate에서 사용해 주었다.
private fun turnScreenOnAndKeyguardOff() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED // deprecated api 27
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD // deprecated api 26
or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // deprecated api 27
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
}
val keyguardMgr = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
keyguardMgr.requestDismissKeyguard(this, null)
}
}
# 안드로이드 12 관련 이슈
- 앱 실행중 & FCM에서 startService했을 때 - FCM오는 메시지의 priority를 High를 변경해주니 실행 된듯
- FCM받았을 때 앱이 종료 되어있는 경우에는 startActivity를 해서 풀스크린으로 보여주었었다. (서비스 실행으로 바꿈)
2-1. 앱 종료 & FCM에서 startService했을 때 - 서비스 실행 되지 않음
: 앱 종료시에도 서비스 실행해줘야하니 WorkManager를 통해 이슈 해결
2-2. 앱종료시에는 풀스크린으로 보여줘야하는데 이 때 notification에 설정한 setFullScreenIntent에 의해서 액티비티가 실행된다.(시스템 UI가 전체 화면 Notification을 띄울 건지, Heads-up Notification을 띄울 건지 선택)
notification을 커스텀하여 remoteview로 노티를 보여주고 있었는데, 커스텀 뷰 클릭시 broadcast로 받아서 startActivity를 해주었는데 Android12에서는 제한하고 있어서 안된다.
12미만 : 기존 broadcast를 이용하여 커스텀뷰 & 이벤트 처리
12이상 : broadcast를 이용할 수 없으니 addAction으로 노티피케이션 시스템 버튼 추가하여 intent되도록 작업
참조 :
https://dongsik93.github.io/til/2020/05/15/til-jetpack-workmanager/
'안드로이드' 카테고리의 다른 글
[Android] FCM을 사용해서 메시지를 보내는 과정 (FCM 추가) (0) | 2024.03.19 |
---|---|
[Android] Exoplayer란? (0) | 2024.03.18 |
푸시 알림 처리를 위한 PendingIntent 및 onNewIntent 사용하기 (0) | 2024.03.14 |
startActivityForResult 및 ActivityResultLauncher에 대한 명확한 이해 (0) | 2024.03.14 |
SingleLiveEvent에 대해 알아보자 (사용법) (2) | 2024.02.14 |