AlarmManager 를 이용한 프로필 업데이트

Android 에서 위젯을 생성하고, 30분마다 프로필을 업데이트 하게 하는 기능은 손쉽게 구현했다. 

다음으로 하루동안 문제를 풀지 않았으면 오후 8시 즈음 노티피케이션을 생성하게 하는 기능 구현을 목표로 잡았다.

solved.ac API 에서 오늘 문제를 풀었는지 여부를 반환해주는 API 는 제공하지 않으므로, 다음과 같은 방식으로 기능을 구현하기로 했다.

 

  1. 매일 12시 정각에 어제 문제 풀이 카운트 수를 업데이트해서 저장 -> 1월 10일 풀이 수가 599라면, 1월 11일 00시에 풀이 수 599를 로드해서 저장한다.
  2. 오후 8시에 풀이 카운트를 다시 불러와 아까 저장한 카운트와 비교해서, 똑같다면 노티피케이션 생성 -> 1월 11일 20시에도 풀이 수가 599라면, 오늘 하루 문제를 풀지 않았다는 뜻으로 관련 노티피케이션을 생성한다.

12시와 오후 8시에 API 를 호출해 프로필을 로딩해야 한다. 이런 예약 작업을 수행하기 위해 AlarmManager 를 이용하기로 결정했다.

 

출처: https://medium.com/androiddevelopers/introducing-workmanager-2083bcfc4712

 

AlarmManager 말고도 구글은 비동기 작업 예약을 위한 다양한 API 를 제공해주고 있다. 위 그림을 살펴보고 나중에 적절한 방법을 이용해 구현해보자

AlarmManager


알람 매니저를 사용하면 지정된 시간 또는 정해진 간격으로 인텐트를 실행할 수 있다. 또한 타이머나 계속 실행 중인 백그라운드 서비스를 사용하지 않고도 작업을 예약할 수 있기 때문에, 리소스 최적화에도 좋은 방법이다.

 

Alarm에는 두 가지 종류가 있는데, 다음과 같다.

 

  • Inexact Alarm 
    • 이름에서도 알 수 있듯이 정확한 시간을 보장하지 않는 알람이다. 대신 시스템이 생각하기에 디바이스의 배터리에 가장 효율적일 시점에 알람을 실행한다고 한다.
  • Exact Alarm
    • 정확한 시점에 알람을 실행하는 것을 보장한다.
    • 그러나 많은 배터리를 소모하기 때문에, 꼭 써야하는 것이 아니라면 사용하지 않는 것이 좋다

 

Alarm 설정하기


Alarm 을 설정하는 2가지 방법이 있다. 첫 번째는 기기가 부팅된 후 경과한 시간을 기반으로 대기 중인 Pending Intent 를 실행하는 방법으로, 알람을 설정한 시간 기준으로 몇 초 뒤에 알람을 시작할 지 설정할 수 있다. 

두 번째는 알람 시작 시간을 직접 지정해주는 방법으로, 12시 30분, 8시 50분 같이 특정 시간에 알람을 시작하도록 설정할 수 있다.

 

이것을 Alarm Type 이라고 부르고, 다음과 같은 목록이 있다.

 

  • ELAPSED_REALTIME - 기기가 부팅된 후 경과한 시간을 기반으로 대기 중인 인텐트를 실행하지만 기기의 절전 모드는 해제하지 않는다.
  • ELASPED_REALTIME_WAKEUP - 기기를 부팅한 후 지정된 시간이 경과하면 기기의 절전 모드를 해제하고 대기 중인 인텐트를 실행한다.
  • RTC - 지정된 시간에 대기 중인 인텐트를 실행하지만 기기의 절전 모드는 실행하지 않음
  • RTC_WAKEUP - 지정된 시간에 기기의 절전 모드를 해제하고 대기 중인 인텐트를 실행한다.

 

정확한 이해를 위해서 우선 ELAPSED REALTIME_WAKEUP 으로 10초 뒤에 알람을 발생하도록 설정해보았다.

우선 Widget 의 onEnabled() 콜백에 삽입할 알람 설정 함수를 구현했다. onEnabled() 콜백은 위젯이 처음 생성될 때 호출되는 함수로, 첫 번째 위젯 인스턴스 생성 후 다음 생성 때부터는 호출되지 않는다.

 

override fun onEnabled(context: Context?) {
        context?.let {
            setAlarm(it)
        }
        super.onEnabled(context)
    }
private fun setAlarm(context: Context) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val alarmIntent = Intent(context, MediumProfileWidgetProvider::class.java).let { intent ->
        intent.action = DAY_CHANGE_ALARM
        PendingIntent.getBroadcast(
            context,
            1004,
            intent,
            PendingIntent.FLAG_CANCEL_CURRENT
        )
    }
    alarmManager.setExact(
        AlarmManager.RTC_WAKEUP,
        System.currentTimeMillis() + 10000,
        alarmIntent
    )
}

우선 context 로부터 AlarmManager 인스턴스를 얻어온다. 그 다음 Alarm  에 의해 실행될 PendingIntent 를 생성한다.

PendingIntent의 수신부에 실제로 인텐트를 수신할 위젯 프로바이더 클래스를 넣어 생성하였다. PendingIntent 의 action 에 구분을 위해 DAY_CHANGE_ALARM 이라는 인텐트 필터를 생성해 주입했다.

 

그 다음 AlarmManager 를 통해 Exact 타입의 알람을 생성하는데, 알람 타입을 RTC_WAKEUP 형식으로 지정했다.

현재 시간을 구해서 10초 뒤에 실행할 알람을 설정한다는 뜻으로, 테스트를 위해 Widget 의 onReceive 콜백에 인텐트의 action 을 토스트 메시지로 띄우도록 구현했다.

 

override fun onReceive(context: Context?, intent: Intent?) {
    context?.let {
        Toast.makeText(it, "방송 수신 + ${intent?.action.toString()}", Toast.LENGTH_SHORT)
        .show()
        ...//
    }
}

실제로 10초 뒤 토스트 메시지가 뜨는 것을 확인할 수 있었다.

 

 

이제 pendingIntent 를 두 개 생성해서, 10초 뒤 오늘 푼 문제 개수를 저장하고, 20초 뒤 푼 문제 개수를 가져와서 비교하는 로직으로 테스트 기능을 구현하였다.

 

fun setDayChangeAlarm(): DefaultAlarmManager = apply {
    val alarmIntent = Intent(context, MediumProfileWidgetProvider::class.java).let { intent ->
        intent.action = MediumProfileWidgetProvider.DAY_CHANGE_ALARM
        PendingIntent.getBroadcast(
            context,
            DAY_CHANGE_CODE,
            intent,
            PendingIntent.FLAG_CANCEL_CURRENT
        )
    }
    alarmManager.setExact(
        AlarmManager.RTC_WAKEUP,
        System.currentTimeMillis() + 10000,
        alarmIntent
    )
}

fun setCountCheckAlarm(): DefaultAlarmManager = apply {
    val alarmIntent = Intent(context, MediumProfileWidgetProvider::class.java).let { intent ->
        intent.action = MediumProfileWidgetProvider.CHECK_SOLVED_COUNT
        PendingIntent.getBroadcast(
            context,
            CHECK_SOLVED_CODE,
            intent,
            PendingIntent.FLAG_CANCEL_CURRENT
        )
    }
    alarmManager.setExact(
        AlarmManager.RTC_WAKEUP,
        System.currentTimeMillis() + 20000,
        alarmIntent
    )
}

 

각 인텐트에 다른 리퀘스트 코드를 사용해 Widget 의 수신부에선 다음과 같이 코드를 작성하였다.

 

DAY_CHANGE_ALARM -> {
    getRecentData(
        it, widgetBuild = false,
        solvedID = getDefaultSolvedID(it)
    ) { profile ->
        putTodaySolvedCount(it, profile.solvedCount)
    }
}
CHECK_SOLVED_COUNT -> {
    getRecentData(
        it, widgetBuild = false,
        solvedID = getDefaultSolvedID(it)
    ) { profile ->
        if (getTodaySolvedCount(it) == profile.solvedCount) {
            Toast.makeText(
                it,
                "어제 푼 문제 : ${getTodaySolvedCount(it)}\n오늘 푼 문제: ${profile.solvedCount}\n문제를 푸셔야겠습니다..",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
    setAlarm(it)
}

 

CHECK_SOLVED_COUNT 가 두 번째로 전달되는 인텐트 액션으로, 여기서 다시 한 번 데이터를 불러와 푼 문제 수를 비교한다. 그리고 만약 같다면 토스트 메시지를 생성했다.

 

 

다음과 같이 알람이 잘 뜨는 것을 확인할 수 있다. 

 

Alarm 취소하기


Widget 의 onDisabled 콜백 함수에서 모든 알람을 해제했다. onDisabled 함수는 모든 위젯 인스턴스가 삭제되었을 때 호출된다. 

이미 설정된 알람은 다음과 같이 취소할 수 있다.

 

val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
//alarmIntent는 이전에 설정한 PendingIntent 이다
alarmManager.cancel(alarmIntent)

alarm 설정 시 설정한 pendingIntent를 AlarmManager 에 넘겨줌으로써 우리는 알람을 취소할 수 있다. 그러나 다른 영역에서 이미 설정한 PendingIntent 를 어떻게 가져올 수 있을까?

 

stackOverFlow 에서 다음과 같은 답변을 찾을 수 있었다. 

  1. 똑같은 리퀘스트 코드와 똑같은 flag를 지닌 Pending Intent 를 생성하자. 
  2. 해당 PendingIntent 를 취소하자
  3. Alarm Manager 에 해당 PendingIntent 를 넘겨줘 취소하자

 

즉, 생성할 때와 동일한 설정값을 가지는 Pending Intent 를 생성해서 이것을 활용해 취소할 수 있다는 것이다. 만약 우리가 다음과 같은 PendingIntent 를 생성했다고 가정해보자

 

val testIntent = Intent(context, TestActivity::class.java).let { intent ->
	PendingIntent.getBroadcast(
    context,
    0, //PendingIntent 의 리퀘스트 코드
    intent,
    PendingIntent.FLAG_CANCEL_CURRENT
}

 

그렇다면 나중에 취소할 때 동일한 리퀘스트 코드인 0과, 동일한 PendingIntent 플래그인 FLAG_CANCEL_CURRENT 를 사용해 인텐트를 생성하고, 이것을 활용해 이전에 설정한 알람을 취소할 수 있는 것이다. 

 

Intent 생성에 사용한 context 는 달라도 상관없다.

 

결론


이로써 알람 매니저를 이용한 위젯 업데이트를 구현해보았다.구글에선 알람 매니저 말고도 WorkManager, GCM Network Manager 등 다양한 비동기 작업 예약 API 를 제공하고 있다. 이번엔 정확한 시간에 예약된 작업을 수행해야 하기 때문에 Alarm Manager 를 사용했다.

 

이제 Notification 을 활용해 사용자에게 문제풀이 여부를 알려주는 기능만 남았다.

 

 

 

'개발 > 백준 프로필' 카테고리의 다른 글

Android Notification 생성하기  (0) 2022.01.13