Skip to content

AlarmManager

yoosumi edited this page Dec 14, 2022 · 6 revisions

WorkerManager와 AlarmManager

현재 시점이 아니라 약속 1시간 전이라는 특정 시간에 알림을 띄우는 동작을 수행하도록 하기 위해서는 백그라운드 작업을 해야 했습니다. 지연된 작업의 경우 WorkManager를 권장하고, 정시에 실행해야 하는 작업은 AlarmManager를 권장하는 공식 문서의 내용이 있었습니다.

이 글만 봤을 때는 WorkManager와 AlarmManager 둘 다 알람 기능을 구현할 수 있겠다는 생각이 들었고, 이 둘의 확실한 차이를 느끼지 못했습니다.

차이를 확실하게 느끼고 싶어 먼저 WorkManager로 알람 기능을 구현해 보았습니다.

private fun reserveNotification(duration: Long, promise: Promise) {
	// WorkManager에 작업 등록
        val data = workDataOf("promiseId" to promise.promiseId,
            "promiseTitle" to promise.title,
            "promiseDate" to promise.date)
        val workManager = WorkManager.getInstance(this)
        val workRequest = OneTimeWorkRequestBuilder<NotificationWorker>()
            .setInitialDelay(duration, TimeUnit.MILLISECONDS)
            .addTag(promise.promiseId)
            .setInputData(data)
            .build()
        workManager.enqueue(workRequest)

    }
class NotificationWorker(val context: Context, params: WorkerParameters): Worker(context, params) {

    override fun doWork(): Result {
	// 푸시 알림 띄우는 동작
        ...
        return Result.success()
    }

}

알람 기능이 잘 작동되는 것처럼 보였지만 앱을 완전히 종료한 상태에서는 WorkerManager가 정상적으로 작동하지 않는 문제가 있었습니다. 지정된 시간이 되어도 푸시 알림은 띄워지지 않고, 다시 앱을 실행했을 때 WorkerManager가 동작해 푸시 알림이 띄워졌습니다.

지연 작업과 정시 작업의 의미를 정확하게 이해할 수 있었고, AlarmManager를 선택해 알람 기능을 구현하게 되었습니다.

구현방식

약속을 생성하거나 수정을 하면 설정한 약속시간을 기준으로 1시간전에 수행시킬 작업을 AlarmManager에 등록했습니다.

val date = promise.date.split(DATE_SPLIT).map { it.toInt() }
val time = promise.time.split(TIME_SPLIT).map { it.toInt() }
val cal = transferToCalendar(date, time)

if (Calendar.getInstance() >= cal) return

val delay = SystemClock.elapsedRealtime() + (cal.timeInMillis - Calendar.getInstance().timeInMillis - ONE_HOUR_IN_MILLIS)
val requestCode = promise.promiseId.hashCode()

alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.ELAPSED_REALTIME_WAKEUP,
    delay,
    getPendingIntent(promise, requestCode)
)

시간을 지정하는 방법에는 4가지가 있습니다.

  • ELAPSED_REALTIME: 기기가 부팅된 후 경과한 시간을 기준
  • ELAPSED_REALTIME_WAKEUP: ELAPSED_REALTIME와 동일하지만 절전모드일 때도 이벤트 발생
  • RTC: UTC 시간을 기준
  • RTC_WAKEUP: RTC와 동일하지만 절전모드일 때도 이벤트 발생

UTC 시간을 사용하면 사용자가 설정한 시간대에 영향을 받을 수 있기 때문에 부팅 시간을 기준으로 설정하는 것이 좋다고 생각했고, 절전모드에서도 알림을 받을 수 있도록 ELAPSED_REALTIME_WAKEUP을 선택했습니다.

AlarmManager엔 다양한 set메서드들이 있습니다. 정확한 알람시간을 보장하고 Doze모드 동안에도 이벤트를 발생시키도록 setExactAndAllowWhileIdle 함수를 사용했습니다.

AlarmManager에 작업을 등록하고 취소할 때 requestCode가 필요합니다. 이 requestCode로 작업을 구분하기 때문에 약속별로 고유한 requestCode를 부여해야 했습니다. 약속정보에는 Promise ID가 존재하는데, 문자열로 이루어진 고유 식별자로 이 값의 hashCode로 requestCode를 부여했습니다.

이후 지정했던 시간이 되면 따로 구현한 BroadcastReceiver 가 이벤트를 받아 알림을 띄우는 동작을 수행합니다.

기기가 다시 시작할 때

기기를 종료하면 AlarmManager에 등록했던 모든 작업들이 취소되기 때문에, 재부팅 시점에 다시 모든 작업을 등록해줘야 합니다.

BroadcastReceiverACTION_BOOT_COMPLETED 이벤트를 받으면 서버에 저장되어 있는 모든 약속 정보들을 가져와 현재 시간보다 이후인 시간만 등록합니다. 이때, 서버에서 정보들을 가져오는 작업이 오래 걸려, ANR이 발생하는 문제가 있었습니다. 이를 해결하기 위해, 정보를 가져오는 작업을 서비스로 따로 구현하고 부팅 이벤트를 받으면 이 서비스를 시작하는 방식으로 구현했습니다.

if (intent.action.equals("android.intent.action.BOOT_COMPLETED")) {
            context.startService(Intent(context, BootService::class.java))
}

결과

약속 시간 1시간전에 알림이 띄워지고, 부팅 후에도 유지되는 것을 확인할 수 있습니다.

Clone this wiki locally